Текст
                    . Шилдт
►► ПРОГРАММИРОВАНИЕ
прекрасное пособие
по новому языку
программирования
OSBORNE
ПИТ ЕР

Г. Шилат C# С^пптер Москва Санкт-Петербург Нижний Новгород Воронеж Ростов-на-Дону Екатеринбург Самара Киев * Харьков * Минск 2003
Содержание Предисловие..........................................................15 Структура книги..................................................... 15 Опыт программирования не обязателен..................................16 Необходимое программное обеспечение..................................16 Нс забудьте: код доступен через Internet.............................16 Для дальнейшего изучения.............................................16 Об авторе........................................................... 17 Глава 1. Основы языка C#.............................................19 Генеалогическое древо языка С*.......................................20 Создание языка С — начало современной эры программирования........20 Появление ООП и создание языка C++................................21 Internet и появление языка Java...................................22 История создания языка C#.........................................23 Связь C# с .NET Framework............................................24 Что такое .NET Framework..........................................24 Как работает не зависящая от языка среда исполнения...............25 Управляемый и не управляемый коды.................................25 Общая языковая спецификация.......................................26 Объектно-ориентированное программирование............................26 Инкапсуляция......................................................27 Полиморфизм.......................................................28 Наследование......................................................29 Первая простая программа.............................................29 Компилирование из командной строки................................30 Использование Visual C++ IDE......................................31 Анализ первой программы...........................................34 Обработка синтаксических ошибок......................................37 Небольшое изменение программы........................................37 Вторая простая программа.............................................38 Другие типы данных...................................................40 Проект 1-1. Преобразование значений температуры 42 Два управляющих оператора............................................43 Оператор if.......................................................43 Цикл for..........................................................45
Содержание 7 Использование блоков кода............................................46 Символ точки с запятой и позиционирование............................48 Использование отступов...............................................48 Проект 1-2. Усовершенствование программы по преобразованию значения температуры 49 Ключевые слова в языке C#.........................................50 Идентификаторы.......................................................50 Библиотека классов C#................................................51 Контрольные вопросы..................................................52 Глава 2. Типы данных и операторы.....................................53 Строгий контроль типов данных в C# ..................................54 Обычные типы данных..................................................54 Целочисленные типы................................................55 Типы данных с плавающей точкой....................................57 Тип decimal.......................................................58 Символы...........................................................59 Булев тип данных..................................................60 Форматирование вывода.............................................61 Проект 2-1. Разговор с Марсом 63 Литералы.............................................................65 Шестнадцатеричные литералы........................................66 Символьные escape-последовательности..............................66 Строковые литералы................................................67 Переменные и их инициализация........................................69 Инициализация переменной..........................................69 Динамическая инициализация........................................70 Область видимости и время жизни переменных...........................70 Операторы............................................................74 Арифметические операторы..........................................74 Операторы сравнения и логические операторы........................76 Проект 2-2. Вывод таблицы истинности логических операторов 79 Оператор присваивания................................................81 Составные операторы присваивания..................................82 Преобразование типа в операциях присваивания......................83 Выполнение операции приведения типа между несовместимыми типами данных. 84 Приоритетность операторов............................................86 Выражения............................................................86 Преобразование типов в выражениях.................................86
8 Содержание Использование пробелов и круглых скобок...........................89 Проект 2-3. Вычисление суммы регулярных выплат по кредиту 90 Контрольные вопросы.................................................93 Глава 3. Управляющие операторы......................................94 Ввод символов с клавиатуры..........................................95 Оператор if.........................................................96 Вложенные операторы if............................................97 Цепочка операторов if-else-if.....................................98 Оператор switch....................................................100 Вложенный оператор switch........................................104 Проект 3-1. Построение простой справочной системы C# 105 Цикл for...........................................................107 Некоторые варианты цикла for.....................................108 Недостающие части цикла for......................................109 Циклы, не имеющие тела.......................................... 111 Объявление управляющих переменных цикла внутри цикла for.......... 111 Цикл while........................................................ 113 Цикл do-while..................................................... 114 Проект 3-2. Совершенствование справочной системы C# 116 Использование оператора break для выхода из цикла................. 118 Использование оператора continue.................................. 121 Оператор goto..................................................... 121 Проект 3-3. Завершение создания справочной системы C# 123 Вложенные циклы....................................................126 Контрольные вопросы................................................128 Глава 4. Классы, объекты и методы..................................130 Основные понятия класса........................................... 131 Общий синтаксис класса.......................................... 131 Определение класса...............................................132 Как создаются объекты..............................................136 Переменные ссылочного типа и оператор присваивания.................137 Методы.............................................................138 Добавление метода к классу Vehicle...............................139 Возврат управления из метода ..................................... 141 Возвращение методом значений.....................................142 Использование параметров.........................................145 Дальнейшее усовершенствование класса Vehicle...................... '^6
Содержание 9 Проект 4-1. Создание справочного класса 148 Конструкторы.......................................................153 Конструкторы с параметрами......................................155 Добавление конструктора к классу Vehicle........................155 Оператор new.......................................................157 Деструкторы и «сборка мусора»......................................157 Деструкторы.....................................................158 Проект 4-2. Демонстрация работы деструкторов 159 Ключевое слово this............................................... 161 Контрольные вопросы................................................163 Глава 5. Подробнее о типах данных и операторах.....................164 Массивы............................................................165 Одномерные массивы..............................................165 Проект 5-1. Сортировка массива 169 Многомерные массивы............................................... 171 Двухмерные массивы............................................. 171 Массивы, имеющие более двух размерностей........................173 Инициализация многомерных массивов..............................173 Невыровненные массивы..............................................174 Присваивание ссылок на массив......................................176 Использование свойства Length......................................177 Проект 5-2. Класс Queue 180 Цикл foreach.......................................................184 Строки.............................................................187 Создание объектов класса String.................................187 Операции со строками............................................188 Массивы строк...................................................190 Неизменность строк..............................................190 Побитовые операторы................................................192 Побитовые операторы AND, OR, XOR и NOT..........................192 Операторы сдвига................................................197 Составные побитовые операторы...................................198 Проект 5-3. Класс ShowBits 199 Оператор ?.........................................................201 Контрольные вопросы................................................204 Глава 6. Детальное рассмотрение методов и классов..................205 Управление доступом к членам класса................................206
10 Содержание Модификаторы в C#.................................................206 Проект 6-1. Усовершенствованный класс Queue 212 Передача объектов методу.............................................213 Как передаются аргументы..........................................214 Использование параметров с модификаторами ref и out..................216 Использование модификатора ref....................................217 Использование модификатора out....................................219 Использование переменного количества аргументов......................221 Возвращение объектов.................................................224 Перегрузка метода....................................................226 Перегрузка конструкторов.............................................232 Вызов перегруженного конструктора с использованием ключевого слова this..234 Проект 6-2. Перегрузка конструктора Queue 235 Метод MainQ..........................................................238 Возвращение значений методом Main()...............................238 Передача аргументов методу Main().................................239 Рекурсия.............................................................241 Ключевое слово static................................................243 Проект 6-3. Алгоритм Quicksort 246 Контрольные вопросы..................................................249 Глава 7. Перегрузка оператора, индексаторы и свойства................250 Перегрузка операторов................................................251 Синтаксис метода оператора........................................251 Перегрузка бинарных операторов....................................252 Перегрузка унарных операторов.....................................254 Дополнительные возможности класса ThreeD..........................258 Перегрузка операторов сравнения...................................262 Основные положения и ограничения при перегрузке операторов........264 Индексаторы..........................................................265 Многомерные индексаторы...........................................270 Свойства.............................................................273 Ограничения в использовании свойств...............................276 Проект 7-1. Создание класса Set 277 Контрольные вопросы..................................................286 Глава 8. Наследование................................................287 Основы наследования..................................................288 Доступ к членам класса при использовании наследования.............291
Содержание 11 Использование модификатора protected..................................294 Конструкторы и наследование...........................................295 Вызов конструкторов наследуемого класса............................297 Скрытие переменных и наследование .....................................301 Использование ключевого слова base для доступа к скрытой переменной.302 Проект 8.1. Расширение возможностей класса Vehicle 304 Создание многоуровневой иерархии классов..............................308 Когда вызываются конструкторы.........................................311 Ссылки на объекты наследуемого и наследующего классов.................312 Виртуальные методы и переопределение..................................317 Для чего нужны переопределенные методы.............................319 Применение виртуальных методов.....................................320 Использование абстрактных классов.....................................324 Использование ключевого слова sealed е целью предотвращения наследования....328 Класс object..........................................................329 Упаковка и распаковка.................................................330 Контрольные вопросы...................................................333 Глава 9. Интерфейсы, структуры и перечисления.........................334 Интерфейсы............................................................335 Реализация интерфейсов.............................................336 Использование интерфейсных ссылок.....................................340 Проект 9.1. Создание интерфейса Queue 342 Свойства интерфейса...................................................347 Интерфейсные индексаторы..............................................348 Наследование интерфейсов..............................................350 Явная реализация......................................................352 Структуры.............................................................354 Перечисления..........................................................356 Инициализация перечисления.........................................358 Указание базового типа перечисления................................358 Контрольные вопросы...................................................359 Глава 10. Обработка исключений........................................360 Класс System.Exception................................................361 Основы обработки исключений...........................................362 Использование блоков try и catch...................................362 Простой пример исключения..........................................363 Второй пример исключения...........................................364
12 Содержание Если исключение не перехвачено.......................................365 Изящная обработка ошибок с помощью исключений........................367 Использование нескольких операторов catch............................368 Перехват всех исключении.............................................369 Вложенные блоки try..................................................370 Генерирование исключений.............................................371 Повторное генерирование исключения................................372 Использование блока finally..........................................373 Более близкое знакомство с исключениями..............................375 Часто используемые исключения.....................................376 Наследование классов исключений......................................377 Перехват исключений производного класса..............................379 Проект 10-1. Добавление исключений в класс Queue 381 Использование ключевых слов checked и unchecked......................383 Контрольные вопросы..................................................387 Глава 11. Ввод/вывод.................................................388 Потоки ввода/вывода C#...............................................389 Байтовые потоки и потоки символов.................................389 Предопределенные потоки...........................................389 Классы потоков.......................................................390 Класс Stream......................................................390 Классы байтовых потоков...........................................391 Классы потоков символов...........................................392 Двоичные потоки...................................................393 Консольный ввод/вывод................................................393 Чтение данных с консоли...........................................394 Вывод данных на консоль...........................................395 Класс FileStream и байт-ориентированный ввод/вывод в файлы...........396 Открытие и закрытие файла.........................................396 Чтение байтов с помощью класса FileStream.........................398 Запись в файл.....................................................399 Ввод/вывод в символьные файлы........................................401 Использование класса StreamWriter.................................402 Использование класса StreamRcadcr.................................404 Перенаправление стандартных потоков..................................405 Проект I l-l. Утилита сравнения файлов 407 Чтение и запись двоичных данных......................................409 Поток BinaryWriter................................................409
Содержание 13 Поток BinaryReader..................................................410 Демонстрация двоичного ввода/вывода.................................411 Произвольный доступ к содержимому файла...............................413 Преобразование числовых строк в их внутренние представления...........415 Проект 11.2. Создание справочной системы 419 Контрольные вопросы...................................................425 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C#.....................................................426 Делегаты..............................................................427 Многоадресность делегатов...........................................431 Преимущества использования делегатов................................433 События...............................................................434 Широковещательное событие...........................................436 Пространства имен.....................................................438 Объявление пространства имен........................................439 Директива using.....................................................441 Вторая форма синтаксиса директивы using.............................443 Аддитивность пространств имен.......................................444 Вложенные пространства имен.........................................445 Пространство имен, используемое по умолчанию........................447 Проект 12-1. Помещение класса Set в пространство имен 447 Операторы преобразования..............................................450 Препроцессор..........................................................455 Директива препроцессора «define.......................................455 Директивы препроцессора «if и «endif..................................456 Директивы препроцессора «else и #elif.................................457 Директива препроцессора #undef........................................459 Директива препроцессора «error........................................460 Директива препроцессора «warning......................................460 Директива препроцессора «line.........................................460 Директивы препроцессора «region и «endregion..........................461 Атрибуты..............................................................461 Атрибут Conditional.................................................461 Атрибут Obsolete....................................................463 Небезопасный код......................................................464 Краткая информация об указателях....................................465 Ключевое слово unsafe...............................................466 Ключевое слово fixed................................................467
14 Содержание Идентификация типа во время работы программы........................468 Проверка типа с помощью ключевого слова is.......................469 Ключевое слово as................................................470 Ключевое слово typeof............................................470 Другие ключевые слова...............................................471 Модификатор доступа internal.....................................471 Ключевое слово sizcof............................................471 Ключевое слово lock..............................................471 Поле readonly....................................................471 Ключевое слово stackalloc........................................472 Оператор using...................................................473 Модификаторы const и volatile....................................473 Что делать дальше...................................................473 Контрольные вопросы.................................................474 Приложение. Ответы на контрольные вопросы...........................475 Глава I: Основы языка C#............................................476 Глава 2: Типы данных и операторы....................................477 Глава 3: Управляющие операторы......................................479 Глава 4: Классы, объекты и методы...................................482 Глава 5: Типы данных и операторы....................................483 Глава 6: Детальное рассмотрение методов и классов...................486 Глава 7: Перегрузка операторов, индексаторы и свойства..............490 Глава 8: Наследование...............................................492 Глава 9: Интерфейсы, структуры и перечисления.......................494 Глава 10: Обработка исключений......................................496 Глава Н: Ввод/вы вод................................................499 Глава 12: Делегаты, события, пространства имен и дополнительные элементы языка C#............................................................502 Алфавитный указатель.................................................504
Предисловие За пару последних лет компьютерный мир несколько сошел с проторенного пути, перейдя от концепции программирования для отдельных систем к созданию инте- рактивного сетевого окружения. Многие годы между компьютерными языками, операционными системами и средствами разработки велась борьба за рынок сбыта, но для большинства из них «сетью по-прежнему оставался один компьютер». Теперь же перед современным языком программирования стоят совершенно новые задачи, для решения которых и был разработан язык С#, являющийся следующей ступенью в эволюции языков программирования. От своих предшественников он взял все самое лучшее, объединил новейшие разработки в области проектирования языков програм- мирования. В первую очередь C# вобрал в себя лучшие качества C++ и Java — двух наиболее широко используемых языков. Кроме того, в него включены такие инно- вационные элементы, как делегаты (delegates) и индексаторы (indexers). А поскольку C# использует средства системы .Net Framework, его код является в высшей степени переносимым и может быть использован при разработке программных комплексов в многоязыковой среде. Компоненты программного обеспечения, созданные с исполь- зованием С#, совместимы с кодом, который написан на других языках, если он также предназначен для .Net Framework. Основная задача книги — научить читателя основам программирования на С#. Для этого применяется пошаговый подход к изучению материала с использованием большого числа примеров и вопросов для самоконтроля, с разработкой множества проектов. Читателю необязательно иметь опыт программирования. Книга начинается с описания таких элементарных операций, как компилирование и запуск Сопро- грамм. Затем в ней обсуждаются ключевые слова, функции и конструкции, которые собственно и составляют язык С#. Ознакомившись с изложенным здесь материалом, вы получите полное представление об основных принципах программирования на С#. Важно понимать, что настоящее издание является всего лишь отправной точкой. С#-программирование — это не только ключевые слова и синтаксис, определяющий язык. Здесь речь должна идти и об использовании библиотеки .Net Framework Class Library, которая настолько обширна, что ее описание требует отдельной книги. Хотя несколько определяемых в библиотеке классов и обсуждается в книге, но по причине ограниченного ее объема о большинстве элементов библиотеки здесь даже не упоминается. Чтобы стать первоклассным программистом на С#, библиотеку следует изучить в совершенстве. Освоив данную книгу, вы получите необходимые знания и для освоения других аспектов языка С#. И последнее: C# — это новый язык. Подобно всем новым компьютерным языкам, он должен пройти период совершенствования и становления. Читателям, надо полагать, будет интересно следить за появлением и развитием его новых свойств и возможностей. Нс удивляйтесь, если он претерпит некоторые изменения. История языка C# только начинается. Структура книги Книгу следует рассматривать как учебное пособие, где каждая последующая глава базируется на предыдущей. Она состоит из 12 глав, в каждой из которых обсуждается отдельный аспект языка С#. А несколько специальных приемов и элементов структуры,
16 Предисловие призванных прочно закрепить изучаемый материал в памяти читателей, делают книгу просто уникальной. О Каждая глава начинается с перечисления рассматриваемых в ней тем. О Завершается каждая глава разделом «Контрольные вопросы», позволяющим читателю самостоятельно проконтролировать, насколько полно усвоен материал. Ответы на задаваемые вопросы приведены в приложении. О В конце каждого основного раздела главы имеется «Минутный практикум», предназначенный для проверки того, как читатель понимает излагаемые положе- ния. Ответы на задаваемые здесь вопросы приведены в нижней части страницы. О Под рубрикой «Ответы профессионала» (оформлена в виде вопроса-ответа) содержится дополнительная информация, а также интересные комментарии к обсуждаемым темам. О Каждая глава содержит один или более проектов, призванных продемонстриро- вать, как на практике применить полученные знания. В основном это примеры реального кода, которые можно использовать в качестве отправной точки при разработке собственных программ. Опыт программирования не обязателен Книга не требует какого-либо опыта программирования. Даже если вам никогда прежде не приходилось заниматься таковым, вы без труда освоите излагаемый здесь материал. Но преобладающая часть читателей наверняка имеет хотя бы небольшой опыт программирования, в первую очередь на языках C++ и Java. А С#, как вы сможете убедиться, довольно тесно связан с обоими этими языками. Таким образом, если вы уже знаете C++ или Java, то гораздо быстрее изучите С#. Необходимое программное обеспечение Для компилирования и запуска программ, приведенных в этой книге, вам понадо- бится Visual Studio.NET 7 (или более поздняя версия), а также инсталлированная система .NET Framework. Не забудьте: код доступен через Internet Помните, что исходный код всех примеров и проектов, описанных в данной книге, можно бесплатно загрузить с Web-узла www.osborne.com. Для дальнейшего изучения Данная книга, как уже было отмечено, входит в большую серию книг о программи- ровании, автором которых является Герберт Шилдт. Ниже приведен перечень изда- ний, которые могут вас заинтересовать. Желающим получить более полные сведения о языке С it рекомендуем приобрести книгу С#: The Complete Reference Для тех, кто хочет изучить C++, особенно полезными окажутся следующие издания: C++: The Complete Reference
Предисловие 17 C++: A Beginner's guide Teach Yourself C++ C++from the Ground Up STL Programming from the Ground Up The C/C++ Programming Annotated Archives А чтобы изучить Java-программирование, можно обратиться к изданиям; Java 2: A Beginner's guide Java 2: The Complete Reference Java 2: The Programmer's Reference Тем, кто желает научиться писать программы для Windows, мы рекомендуем следую- щие книги Герберта Шиллта: Windows 98 Programming from the Ground Up Windows 2000 Programming from the Ground Up MFC Programming from the Ground Up The Windows Programming Annotated Archives Если же вы хотите изучить язык С, являющийся основой всех современных языков программирования, обратитесь к книгам: С: The Complete Reference Teach Yourself C Каждый, кому нужны более фундаментальные знания и полные ответы, может обратиться к Герберту Шилдту, признанному специалисту в области программиро- вания. Об авторе Один из самых популярных авторов книг по программированию, Герберт Шилдт, считается ведущим специалистом по языкам С, C++, Java и С#, а также экспертом в области создания программ для Windows. Написанные им книги по программированию были переведены на многие языки и изданы общим тиражом более 3 миллионов экземпляров. Герберт Шилдт является автором огромного числа изданий, ставших бестселлерами. Наиболее известные среди них — это C++: The Complete Reference, Java 2: The Complete Reference, Java 2: A Beginner's Guide, Windows 2000 Programming from the Ground Up и C: The Complete Reference. Шилдт имеет степень мастера компьютерных наук, присвоенную ему университетом штата Иллинойс. Связаться с Гербертом можно через его консультационный офис по телефону (217) 548-4683.
глава Основы языка C# □ История создания языка C# □ Использование среды .NET Framework □ Три принципа объектно-ориентирован- ного программирования □ Создание, компилирование и запуск Сопрограмм □ Использование переменных □ Работа с операторами if и for □ Использование блоков кода □ Ключевые слова C#
20 Глава 1. Основы языка C# Языки программирования служат самым разнообразным целям — от решения слож- ных математических задач и проведения экономико-математических расчетов до создания музыкальной партитуры и машинной графики. Попытки создания совер- шенного языка программирования предпринимаются столько же лет, сколько суще- ствует само программирование. В результате этого поиска компанией Microsoft был разработан язык С#, соответствующий современным стандартам программирования и предназначенный для поддержки развития технологии .NET Framework. Этот язык предоставляет эффективный метод написания программ для современных компью- терных систем предприятий, которые используют операционную систему Windows и компоненты Internet. Цель нашей книги — помочь вам научиться программировать на языке С#. В этой главе мы расскажем об истории создания языка C# и опишем наиболее важные его свойства. Безусловно, изучение этого языка представляет определенные трудно- сти, поскольку все его элементы тесно взаимосвязаны и не могут рассматриваться изолированно друг от друга. Чтобы справиться с этой проблемой, в данной главе мы кратко рассмотрим общую структуру Сопрограмм, а также основные управляющие элементы и условные операторы языка, сфокусировав внимание на базисных кон- цепциях программирования, общих для любой Сопрограммы. Генеалогическое древо языка C# Компьютерные языки взаимосвязаны, а нс существуют сами по себе. Каждый новый язык в той или иной форме наследует свойства ранее созданных языков, то есть осуществляется принцип преемственности. В результате возможности одного языка используются другими (например, новые характеристики интегрируются в уже суще- ствующий контекст, а старые конструкции языка удаляются). Так происходит эво- люция компьютерных языков и совершенствуется искусство программирования. Язык C# не является исключением, он унаследовал много полезных возможностей от других языков программирования и напрямую связан с двумя наиболее широко применяемыми в мире компьютерными языками — С и C++, а также с языком Java. Для понимания C# следует разобраться в природе такой связи, поэтому сначала мы расскажем об истории развития этих трех языков. Создание языка С — начало современной эры программирования Язык С был разработан Дэннисом Ричи, системным программистом из компании Bell Laboratories, город Мюррей-хилл, штат Нью-Джерси, в 1972 году. Этот язык настолько хорошо зарекомендовал себя, что в конечном счете на нем было написано более 90 % кода ядра операционной системы Unix (которую поначалу писали на языке низкого уровня — ассемблере). К моменту появления С более ранние языки, самым известным из которых является Pascal, использовались достаточно успешно, но именно язык С определил начало современной эры программирования. Революционные изменения в технологии программирования, приведшие к появле- нию структурного программирования 1960-х годов, обусловили базовые возможности для создания языка С. До появления структурного программирования трудно было писать большие программы, поскольку с увеличением количества строк код программы
Генеалогическое древо языка СИ 21 превращался в запутанную массу переходов, вызовов и возвращаемых результатов, за которыми сложно было проследить ход самой программы. Структурные языки решили эту проблему, добавив в инструментарий программиста условные операторы, процедуры с локальными переменными и другие усовершенствования. Таким обра- зом появилась возможность писать относительно большие программы. Именно язык С стал первым структурным языком, в котором успешно сочетались мощность, элегантность и выразительность. Такие его свойства, как краткость и легкий в использовании синтаксис в сочетании с принципом, согласно которому ответственность за возможные ошибки возлагается на программиста, а не на язык, быстро нашли множество сторонников. Сегодня мы считаем эти качества само собой разумеющимися, но тогда в С впервые были воплощены великолепные новые возможности, так необходимые программистам. В итоге с 1980-х годов С является самым используемым языком структурного программирования. Но по мере развития программирования появляется проблема обработки программ все большего размера. Как только код проекта достигает определенного объема (его числовое значение зависит от программы, программиста, используемых инструмен- тов, но приблизительно речь идет о 5000 строк кода) возникают трудности в понимании и сопровождении С-программы. Появление ООП и создание языка C++ В конце 1970-х годов настал момент, когда многие проекты достигли максимального размера, доступного для обработки с помощью языка структурного программирова- ния С. Теперь требовались новые подходы, и для решения этой проблемы было создано объектно-ориентированное программирование (ООП), позволяющее програм- мисту работать с программами большего объема. А поскольку С, являвшийся в то время самым популярным языком, не поддерживал ООП, возникла необходимость создания его объектно-ориентированной версии (названной позднее С++). Эта версия была разработана в той же компании Bell Laboratories Бьярном Страуст- рапом в начале 1979 года. Первоначально новый язык получил название «С с классами», но в 1983 году был переименован в C++. Он полностью включает в себя язык С (то есть С служит фундаментом для C++) и содержит новые возможности, предназначенные для поддержки объектно-ориентированного программирования. Фактически C++ является объектно-ориентированной версией языка С, поэтому программисту, знающему С, при переходе к программированию на C++ надо изучить только новые концепции ООП, а не новый язык. Долгое время язык C++ развивался экстенсивно и оставался в тени. В начале 1990-х годов он начинает применяться массово и приобретает огромную популярность, а к концу десятилетия становится наиболее широко используемым языком разработки программного обеспечения, лидирующим и сегодня. Важно понимать, что разработка C++ нс является попыткой создания нового языка программирования, а лишь совершенствует и дополняет уже достаточно успешный язык. Такой подход, ставший новым направлением развития компьютерных языков, используется и сейчас.
22 Глава 1. Основы языка C# Internet и появление языка Java Следующим большим достижением в развитии языков программирования стал язык Java. Работа над Java, который изначально назывался Oak, началась в 1991 году в компании Sun Microsystems. Основными разработчиками этого языка были Джеймс Гослинг, Патрик Нотой, Крис Ворт, Эд Франк и Майк Шеридан. Java является структурным объектно-ориентированным языком с синтаксисом и стратегией, взятыми из языка C++. Инновации Java были обусловлены бурным развитием информационных технологий и стремительным увеличением количества пользователей Internet, а также совершенствованием технологии программирования. До широкого распространения Internet большинство написанных программ компи- лировались для конкретных процессоров и определенных операционных систем. И хотя программисты при написании удачной программы практически всегда задавались вопросом повторного использования кода, этот вопрос не был первоочередным. Однако с развитием Internet (то есть появлением возможности соединения через сеть компью- теров с различными процессорами и операционными системами) на первый план вышла именно проблема легкого переноса программ с одной платформы на другую. Для решения этой задачи необходим был новый язык, которым и стал Java. Нужно отметить, что сначала Java отводилась более скромная роль, он создавался как не зависящий от платформы язык, который можно было бы применять при создании программного обеспечения для встроенных контроллеров. В 1993 году стало очевидным, что технологии, использовавшиеся для решения проблемы переносимо- сти в малом масштабе (для встроенных контроллеров), можно использовать в большом масштабе (для Internet). Самая главная возможность Java — способность создания межплатформенного переносимого кода — и стала причиной быстрого распространения этого языка. В Java переносимость достигается посредством транслирования исходного кода программы в промежуточный язык, называемый башп-кодом, который затем выпол- няется виртуальной машиной Java (Java Virtual Machine, JVM). Следовательно, Java-программа может быть запущена на любой платформе, имеющей JVM. А по- скольку JVM относительно легко реализуется, она доступна для большого числа платформ. Использование байт-кода в Java радикально отличается от применения кодов в языках С и C++, практически всегда компилируемых в исполняемый машинный код. Машинный код связан с определенным процессором и операционной системой, следовательно, для запуска С/С++-программы на других платформах необходимо перекомпилировать исходный код программы в машинный код каждой их этих платформ (то есть нужно иметь несколько различных исполняемых версий програм- мы). Понятно, что это трудоемкий и дорогой процесс. А в Java было предложено элегантное и эффективное решение — использование промежуточного языка. И это же решение в дальнейшем было применено в С#. Уже говорилось, что в соответствии с новым подходом авторы Java создали его на основе С и C++ (синтаксис Java базируется на С, а объектная модель развилась из C++). Хотя Java-код не совместим с С или C++, его синтаксис сходен с синтаксисом этих языков, поэтому большая часть программистов, использовавших С и C++, смогла перейти на Java без особых усилий. Как Страустрапу при разработке C++, так и авторам Java не понадобилось создавать совершенно новый язык, они использовали
Генеалогическое древо языка СИ 23 в качестве базовых уже известные языки и смогли сосредоточить внимание на инновационных элементах. Стоит отмстить, что после появления Java языки С и C++ стали общепринятым фундаментом для создания новых компьютерных языков. История создания языка C# Хотя язык Java решил многие проблемы переносимости программ с одной платформы на другую, все же для успешной работы в современном Internet-окружении ему недостает некоторых свойств, одним из которых является поддержка возможности взаимодействия нескольких компьютерных языков (многоязыкового программиро- вания). Под многоязыковым программированием понимается способность кода, напи- санного на разных языках, работать совместно. Эта возможность очень важна при создании больших программ, а также желательна при программировании отдельных компонентов, которые можно было бы использовать во многих компьютерных языках и в различной операционной среде. Серьезным недостатком Java является отсутствие прямой поддержки платформы Windows, являющейся сегодня наиболее широко используемой операционной систе- мой в мире. (Хотя Java-программы могут выполняться в среде Windows при наличии инсталлированной JVM.) Чтобы решить эти проблемы, компания Microsoft в конце 1990-х годов разработала язык C# (главный архитектор языка Андерс Хейльсберг), являющийся составной частью общей стратегии .NET этой компании. Альфа-версия языка была выпущена в середине 2000 года. Язык C# напрямую связан с широко применяемыми и наиболее популярными во всем мире языками программирования С, C++ и Java. Сегодня практически вес профессиональные программисты знают эти языки, поэтому переход к базирующе- муся на них C# происходит без особых трудностей. Хейльсберг, так же как авторы языков C++ и Java, нс «изобретал колесо», а пошел по проторенному пути — используя в качестве фундамента ранее созданные языки, сосредоточился на улуч- шениях и инновациях. Генеалогическое древо C# показано на рис. 1,1. Язык C# строится на объектной модели, которая была определена в C++, а синтаксис, многие ключевые слова и операторы он унаследовал от языка С. Если вы знаете эти языки программирования, то у вас не возникнет проблем при изучении С#. Связь между C# и Java более сложная. Оба языка разработаны для создания переносимого кода, базируются на С и C++, используют их синтаксис и объектную модель. Однако между этими языками нет прямой связи, они больше похожи на двоюродных братьев, имеющих общих предков, но отличающихся многими призна- кам. Если вы умеете работать на Java, это облегчит вам освоение С#, и наоборот, при изучении Java вам пригодится знание многих концепций С#. Язык C# содержит многие инновационные свойства, которые будут рассматриваться в этой книге. Сразу заметим, что некоторые из его наиболее важных нововведений относятся к встроенной поддержке компонентов программного обеспечения. То есть фактически C# создан как компонентно-ориентированный язык, включающий, например, элементы (такие как свойства, методы и события), непосредственно поддерживающие составные части компонентов программного обеспечения. Но
24 Глава 1. Основы языка C# пожалуй, наиболее важная новая характеристика C# — способность работать is многоязыковом окружении. Связь C# с .NET Framework Хотя C# как язык программирования может изучаться изолированно, лучше рассмат- риватьего во взаимосвязи с .NET Framework — средой, в которой он работает. Потому что, во-первых, C# изначально разрабатывался компанией Microsoft для создания кода .NET Framework, во-вторых, .NET Framework определяет библиотеки, исполь- зуемые языком С#. Поскольку они так тесно связаны, важно понимать общую концепцию .NET Framework и ее значимость для С#. Что такое .NET Framework Отвечая на вопрос, вынесенный в заголовок, видимо, можно сказать, что .NET Framework определяет среду, которая поддерживает развитие и выполнение платфор- мо^сзависимых приложений. Она допускает совместную работу в приложении раз- личных языков программирования, а также обеспечивает переносимость программ и общую модель программирования для Windows. Важно отмстить, что .NET Frame- work не ограничена платформой Windows и написанные для этой платформы про- граммы могут быть в будущем перенесены на другие платформы. Язык C# использует две важные составляющие .NE7' Framework. Первая — это не зависящая от языка среда исполнения (Common Language Runtime, CLR), система, управляющая исполнением вашей программы, и являющаяся частью технологии .NET Framework, которая позволяет программам быть переносимыми, поддерживает программирование с использованием нескольких языков и обеспечивает безопас- ность передачи данных. Вторая составляющая — библиотека классов .NET, которая дает программе доступ к среде исполнения, например, используется для ввода/вывода данных. Если вы начинающий программист, то вам может быть не знаком термин класс. Подробно .мы расскажем о классах немного позже, а сейчас просто скажем, что классом является объектно-ориен- тированная конструкция, которая помогает организовать программу. Пока ваша про- грамма ограничена характеристиками, определенными библиотекой классов .NET, она может быть запущена везде, где поддерживается среда исполнения .NEL
Связь C# с .NET Framework 25 Как работает не зависящая от языка среда исполнения Нс зависящая от языка среда исполнения (CLR) управляет выполнением кода .NET. Расскажем, как она работает. При компилировании Сопрограммы мы получаем не исполняемый код, а файл, содержащий специальный тип псевдокода, называемый промежуточным языком Microsoft (Microsoft Intermediate Language, MSIL). Язык MSIL определяет набор переносимых инструкций, нс зависимых от конкретного процессора, то сеть, по существу, MSIL определяет переносимый язык ассемблера. Обратим ваше внимание на то, что концептуально промежуточный язык Microsoft похож на байт-код Java, но между ними имеются различия. Система CLR транслирует промежуточный код в исполняемый во время запуска программы. Любая программа, компилированная в MSJL, может быть запушена в любой операционной системе, для которой реализована среда CLR. Это часть механизма, с помощью которого в .NET Framework достигается переносимость программ. Промежуточный язык Microsoft превращается в исполняемый код при использовании J IT-компилятора (англ, just in time — в нужный момент). Процесс работает следую- щим образом: при выполнении .N'ET-программы система CLRактивизирует JIT-ком- пилятор, который затем превращает MSIL во внутренний код данного процессора, причем коды частей программы преобразуются по мере того, как в них возникает потребность. Следовательно, ваша Сопрограмма фактически исполняется как внут- ренний код, хотя изначально она компилировалась в MSIL. Это означает, что время запуска вашей программы практически такое же, как если бы она была сразу скомпилирована во внутренний код, но при этом у вас появляется преимущество MSIL — переносимость программы. Кроме того, при компилировании Сопрограммы в дополнение к MSIL вы получаете еще один компонент — метаданные, которые описывают данные, используемые вашей программой, и позволяют вашему коду взаимодействовать с другим кодом. Метаданные содержатся в том же файле, что и MS1L. В принципе этих знаний о среде CLR, MSIL и метаданных достаточно для реализации основной части задач программирования на языке С#, поскольку этот язык само- стоятельно организует работу надлежащим образом. Управляемый и не управляемый коды В целом, когда вы пишите С#-програ.мму, то создаете так называемый управляемый код, который выполняется под контролем не зависящей от языка среды исполнения (CLR). Поскольку программа запускается под контролем CLR, управляемый! код должен соответствовать определенным требованиям, при выполнении которых он получает множество преимуществ, включая современное управление памятью, спо- собность совмещать языки, высокий уровень безопасности передачи данных, под- держку контроля версии и понятный способ взаимодействия компонентов программ- ного обеспечения. Требования, которым должен соответствовать управляемый код, следующие: компилятор должен создать MSIL-файл, предназначенный для CLR, а также использовать библиотеки .NET Framework (и то, и другое делает С#). Альтернативой управляемому коду является не управляемый код, который не выпол- няется CLR. До появления .NET Framework все Windows-программы использовали не управляемый код, сейчас же оба вышеназванных кода могут работать вместе, и язык С#, генерируя управляемый код, способен взаимодействовать с созданными ранее программами.
26 Глава 1. Основы языка C# Общая языковая спецификация Вес преимущества управляемого кода обеспечиваются CLR. Если же ваш код будет использоваться программами, написанными на других языках, для максимальной совместимости необходимо придерживаться общей языковой спецификации (Com- mon Language Specification, CLS). Эта спецификация описывает набор характеристик, являющихся общими для различных языков. Соответствие кода обшей языковой спецификации особенно важно при создании компонентов программного обеспече- ния, которые будут использоваться другими языками. Если в будущем вам придется создавать коммерческий код, то вы должны будете придерживаться спецификации CLS, поэтому мы привели информацию о ней в данном разделе, хотя это и не имеет непосредственного отношения к нашей книге. Ответы Вопрос. За — - - — — >........— 1ем для решения вопросов переносимости, безопасности и про- граммирования с использованием нескольких языков нужно было создавать новый компьютерный язык С#? Нельзя ли было адаптировать язык C++ для поддержки .NET Framework? Ответ. Да, язык C++ можно адаптировать так, чтобы он создавал .NET-co- вместимый код, запускаемый под управлением среды CLR. Первоначально компания Microsoft так и поступила, добавив к C++ так называемые управляемые расширения, которые сделали возможным перенос существующего кода в .NET Framework. Но новая разработка .NET позволяет намного легче выполнить это в С#. Помните, что язык C# специально разрабатывался для платформы .NET и предназначен для поддержки развития этой технологии. Минутный практикум I. С какими языками язык C# имеет непосредствснныесвязи? 2. Что такое не зависящая от языка среда исполнения (CLR)? 3. Что такое JIT-компилятор? Объектно-ориентированное программирование Язык C# базируется на принципах объектно-ориентированного программирования (ООП), и вес С#-программы являются в какой-то степени объектно-ориентирован- ными. Поэтому для написания даже самой простой С#-программы очень важно знать принципы ООП. ООП является мощной технологией выполнения задач, с которыми приходится сталкиваться программистам. Со времен изобретения компьютера методы програм- мирования значительно изменились. На протяжении развития компьютерной науки 1. Язык C# являтся потомком С и C++, а также имеет родство с Java. 2. Не зависящая от языка среда исполнения (CLR) управляет выполнением .NET-программ. 3. J IT-компилятор преобразует MSlL-код во внутренний код данного процессора, причем коды частей программы преобразуются по мере того, как в них возникает потребность.
Объектно-ориентированнов программирование 27 специалистам в основном приходилось решать вопросы, связанные с увеличением сложности программ. Например, на первых компьютерах программирование осуще- ствлялось посредством изменения двоичных машинных инструкций, при этом ис- пользовались элементы управления передней панели компьютера. Такой метод хорошо работал, пока объем программы ограничивался несколькими сотнями инст- рукций. Когда же объем используемого кода вырос, был изобретен язык ассемблера, и программист, используя символическое представление машинных инструкций, уже смог работать с более сложными и громоздкими программами. Затем, приспосабливаясь к росту объема и сложности программ, программисты разработали высокоуровневые языки, такие как FORTRAN и COBOL. Когда же эти ранние языки достигли предела своих возможностей, было изобретено структурное программирование. Заметьте, что на каждом этапе развития программирования создавались технологии и инструменты, позволяющие программисту решать все более сложные задачи. С каждым шагом на этом пути новые технологии вбирали в себя все самое лучшее от предыдущих. Настал момент, когда многие проекты приблизились к той границе, где структурное программирование уже не могло соответствовать предъявляемым требованиям, и возникла необходимость в принципиально новой прогрессивной технологии, которой и стало объектно-ориентированное программирование. ООП вобрало в себя лучшие идеи структурного программирования и объединило их с несколькими новыми концепциями, в результате чего появился новый способ организации программ. Не вдаваясь в подробности, можно сказать, что программа составляется одним из двух способов: вокруг своего кода или вокруг своих данных. При использовании технологии структурного программирования программы обычно организуются вокруг кода. Такой метод можно рассматривать как «код, воздействую- щий на данные». В ООП программы работают иным способом. Они организованы вокруг данных, и их ключевой принцип можно сформулировать как «контролируемый данными доступ к коду». В объектно-ориентированном языке вы определяете данные, а также процедуры, которые могут воздействовать на эти данные (то есть именно тип данных задаст виды операций, которые могут быть применены к этим данным). Для поддержки принципов объектно-ориентированного программирования у всех языков ООП, включая С#, есть три общие черты — инкапсуляция, полиморфизм и наследование. Давайте рассмотрим их в отдельности. Инкапсуляция Инкапсуляция — это механизм программирования, связывающий воедино код и данные, которыми он манипулирует, а также ограждающий их от внешнего доступа и неправильного применения. В объектно-ориентированном языке код и все необ- ходимые данные могут связываться таким способом, что создается автономная структура — объект. Другими словами, объектом является структура, поддерживаю- щая инкапсуляцию. В пределах объекта код, данные или и код, и данные могут быть либо закрытыми для Других объектов (private), либо открытыми (public). Закрытые код и данные известны и доступны из другой части только этого же объекта (то есть к закрытому коду и данным не может быть осуществлен доступ из той части программы, которая существует за пределами данного объекта). Когда же код и
28 Глава 1. Основы языка C# данные открыты (объявлены в рамках какого-либо объекта как public), другие части программы тоже имеют возможность с ними работать. Обычно части объекта, объявленные как public, используются для обеспечения контролируемого интер- фейса с закрытыми элементами объекта. В C# основной структурой, использующей инкапсуляцию, является класс, специфи- цирующий как данные, так и код, который будет работать с этими данными. Язык C# применяет спецификацию классов для конструирования объектов, являющихся экземплярами классов. Следовательно, класс фактически является набором инструк- ций, в которых указано, как должен быть создан объект. Код и составляющие класс данные называются членами класса, а данные, определен- ные классом, называются переменными экземпляра. Фрагменты кода, которые рабо- тают с этими данными, называются методами члена класса или просто методами. В C# термин метод принят для подпрограммы, которую в С/О+ программисты называют функцией (поскольку C# является прямым наследником C++, то иногда используется и термин функция). Полиморфизм Полиморфизм (от греческого «множество форм») является свойством, которое по- зволяет нескольким объектам, имеющим некоторые общие характеристики, получать доступ к одному интерфейсу и реализовывать упомянутые в нем методы. В качестве простого примера рассмотрим руль автомобиля. Руль (интерфейс) остается одинако- вым независимо от того, какой тип рулевого механизма фактически используется. То есть принцип работы руля всегда остается неизменным (например, поворот рулевого колеса влево всегда приводит к повороту автомобиля влево), хотя на автомобиле может быть установлено ручное управление или рулевое управление с усилителем. Преимуще- ство унифицированного интерфейса состоит в том, что если вы один раз научились водить автомобиль, то сможете управлять любым типом автомобиля. Тот же принцип может быть применен в программировании. Например, рассмотрим стек — список типа LIFO (last in, first out). Вы можете работать с программой, которой требуется три стека различных типов (один для целочисленных значений, один для значений с плавающей запятой и один для символов). В этом случае для реализации каждого стека используется один и тот же алгоритм, хотя хранящиеся данные различны. В не объектно-ориентированном языке вам понадобилось бы создавать три различных набора стековых процедур с собственным именем для каждого набора. А в C# благодаря полиморфизму можно создать общий набор стековых процедур, которые подойдут для работы со всеми тремя типами стеков. Таким образом, определив методы, использующие один тип стека, вы сможете применять их и для остальных типов. Обобщенно концепцию полиморфизма можно выразить так: «один интерфейс — множество методов». Это означает, что для группы похожих процессов можно создать унифицированный интерфейс. Полиморфизм позволяет уменьшать сложность про- грамм путем использования единого интерфейса для спецификации общего класса действий. Применительно к каждой ситуации компилятор сам выбирает специфиче- ское действие (то есть метод), избавляя вас от необходимости делать это вручную. Вы только должны помнить, какие методы упомянуты в данном интерфейсе, и реализо- вывать их.
Первая простая программа 29 Наследование Наследование — это свойство, с помощью которого один объект может приобретать свойства другого. При этом поддерживается концепция иерархической классифика- ции, имеющей направление сверху вниз. Например, можно сказать, что класс Вкусное Красноеяблоко является частью класса яблоки, который в свою очередь является частью класса фрукты, принадлежащего классу продукты. То есть класс продукты обладает определенными качествами (съедобные, питательные и так далее), которые также применимы к его подклассу фрукты. В дополнение к этим качествам класс фрукты имеет свои специфические характеристики (сочные, сладкие и так далее), отличающие его от других продуктов. Класс яблоки определяет качества, которые присущи яблокам (растут на дереве, не являются тропическими фруктами и так далее). Класс Вкусное_Красное_яблоко в свою очередь наследует все качества предшествующих классов и определяет только те качества, которые делают его уникальным. Без использования наследования каждый объект должен явно определять все свои характеристики; используя наследование, объект должен определить только те качества, которые делают его уникальным в пределах своего класса. Он может наследовать общие атрибуты от своих родительских классов. Следовательно, именно механизм наследования дает возможность одному объекту быть специфическим экземпляром своего класса. Минутный практикум ° 1- Назовите принципы ООП. 2. Что является базовым элементом инкапсуляции? профессионала Вопрос. Вы утверждаете, что ООП является эффективным способом управ- ления большими программами, но создается впечатление, что он может излишне усложнить относительно небольшие программы. Поскольку вы говорите, что все Сопрограммы являются в какой-то степени объектно-ориентированными, не воз- никнут ли неудобства при написании небольших Сопрограмм? Ответ. Нет, дальше вы увидите, что для небольших объемов кода использо- вание ООП в C# практически не отражается на сложности программ. Хотя C# следует строгой объектной модели, вы имеете достаточную свободу в определении степени ее применения. Для небольших программ «объектная ориентированность» едва ощутима, по мерс увеличения объема программы в нее можно ин гсгрировать большее количество объектно-ориентированных свойств. Первая простая программа Перед началом углубленного изуч ия деталей языка скомпилируем и запустим короткую и простую программу, написанную на С#. /* Это простая программа, написанная на С#. 1. Принципами ООП являются инкапсуляция, полиморфизм и наследование. 2. Базовым элементом инкапсуляции является класс.
30 Глава 1. Основы языка C# Файл, содержащий код программы, — Example.cs. */ using System; class Example { // Сопрограмма начинается с вызова метода Main(). public static void Main О t Console.WriteLine("Простая C#-программа. ; } Существует два способа компилирования, запуска и редактирования С#-программ. Во-первых, вы можете выполнить компиляцию из командной строки, запустив программу csc.exe. Во-вторых, можете использовать интегрированную среду разра- ботки Visual C++ (Integrated Development Environment, IDE). Оба эти способа описаны ниже. Компилирование из командной строки Компилирование из командной строки представляет собой более легкий способ работы с большинством из приведенных в книге Сопрограмм, хотя для коммерче- ских проектов вы, вероятно, будете использовать интегрированную среду разработки Visual C++. Для компилирования и запуска Сопрограмм из командной строки вам необходимо произвести три действия. 1. Создать файл, содержащий код программы. 2. Компилировать программу. 3. Запустить программу. Создание файла Представленные в этой книге программы можно загрузить с Web-узла компании Osborne www.osborne.com или при желании ввести код программы вручную. При ручном вводе загрузите программу в файл, используя текстовый редактор (например. Notepad). Текстовые ASCII-файлы должны быть созданы без элементов форматиро- вания, поскольку такая информация может привести к ошибкам при компилирова- нии. После ввода программы присвойте файлу имя Example.es. Компилирование программы Для компилирования программы необходимо запустить компилятор C# csc.exe, указав имя исходного файла в командной строке: C:\>csc Example.cs Компилятор esc создает файл Example.exe, содержащий MSIL-версию программы. Хотя MSIL нс является исполняемым кодом, он содержится в исполняемом (ехе) файле. При попытке запуска файла Example.exe не зависящая от языка среда исполнения (CLR) автоматически активизирует ЛТ-компилятор. Но если вы попы- таетесь выполнить файл Example.exe (или любой другой файл, содержащий MS1L) на компьютере, где не инсталлирована .NET Framework, программа нс будет работать, поскольку отсутствует CLR.
Первая простая программа 31 Примечание Возможно, перед запуском компилятора esc. ехе вам нужно будет запустить команд- ный файл vcvars32.bat, который обычно находится в каталоге \Program Files\Mic- rosoft Visual Studio.NET\Vc7\Bin. В качестве альтернативы можно активизировать процесс компилирования из командной строки, выбрав элемент Visual Studio.NET Command Prompt из списка инструментов, приведенных в подменю Microsoft Visual Studio.NET 7.0, находящемся в меню Пуск | Программы на панели задач. Запуск программы Для запуска программы введите ее имя в командной строке, как показано ниже: С:\>Example Когда программа будет запущена, на экране появится следующая строка: Простая C#-программа. Использование Visual C++ IDE Начиная с версии Visual Studio 7, Visual C++ IDE может компилировать С#-програм- мы. Для редактирования, компилирования и запуска СП-программ с использованием Visual C++ IDE вам необходимо произвести действия, перечисленные ниже. (Если на вашем компьютере инсталлирована другая версия Visual C++, возможно, порядок действий будет другим.) 1. Создайте новый пустой СП-проект, выбрав пункты меню File | New | Project. Далее выберите папку Visual C# Project, а затем пиктограмму Empty Project, как показано на рисунке. 3 . Projert Jvpes: Saw®:' Location: «Ho® ;New Project Bigg ge riF? fl cunrroruurary Windows Console | Project! | C:\Docunientsand Setttngs^pada^Mo^^ grower. • -hl Visual Basic Projects €3 Cj Setup and Deployment Projects l+l- Other Projects C_1 Visual Studio Solutions Templates! ASP,NET Web Application A5P.NET Web Service Web Control Library Project (МЙ be created at C:y.,|Pada#<a|Mc*i докуненть|^У1яй Studio ProjectslPra)ectl. Carrel Ld Samples Visual Studio Sarnoles
Глааа 1. Основы языка с» 2. После создания проекта щелкните правой кнопкой мыши на имени проекта в окне Solution. В появившемся контекстном меню выберите пункт Add. затем пункт Add New Item. На экране вашего .монитора появится следующая картинка. 3. Когда откроется диалоговое окно Add New Item, выберите пункт Ixical Project Items, затем пиктограмму C# Code File. Изображение на экране вашего монитора будет следующим. i...............„ ................................ i...................Л....... . . ' . ИВпуск|! ] ЙЖЙ Й |Й' у ! i|-*'Pro>cctl Microsoft Vi... 13:03
Первая простая программа 33 4. Загрузите программу и сохраните файл под именем Example, cs. (Помните, что программы, приведенные в этой книге, можно загрузить с Web-узла www.os- borne.com.) На экране вашего монитора появится следующее изображение. 5. Скомпилируйте программу, выбрав пункт Build из меню Build. б. Запустите программу, выбрав пункт Start Without Debugging из меню Debug. Когда программа будет запущена, появится окно, показанное на рисунке. Projects - Microsoft Visual C# .NET [design] - Examplexs удал . Effect Qebug lot-fc J&indow 1ЙР *' isM' ft ij D:\BooksXprogSfcxample. exe Простая программа Введите любой символ \ Командная строка - ЯВПУО<jj Д ' ' jl^Proiecta Microsoft Vi... йдг
34 Глава 1. Основы языка C# Для компилирования и запуска программ, приводимых в этой книге, не обязательно каждый раз создавать новый проект, можно использовать тот же С#-проект. Для этого просто удалите текущий файл и добавьте новый, затем скомпилируйте его и запустите. Как уже говорилось ранее, короткие программы, представленные в первой части книги, проще компилировать из командной строки. Анализ первой программы Хотя программа Example.cs достаточно короткая, она включает несколько ключе- вых характеристик, свойственных всем С#-программам. Давайте рассмотрим каждую часть программы, начиная с ее имени, более внимательно. В отличие от некоторых языков программирования (например, Java), в которых очень важно правильно назвать файл, содержащий код программы, C# позволяет выбрать имя программы произвольно. Мы предложили назвать нашу программу Example. os, но для C# было бы приемлемо и любое другое имя. Например, предыдущий файл программы можно назвать Sample.cs, Test.cs или даже X.cs. По соглашению файлы Сопрограмм имеют расширение .cs. Но многие програм- мисты называют файл по имени главного класса, определенного в рамках файла, вот почему мы выбрали для файла имя Example.cs. Поскольку имя Сопрограммы выбирается произвольно, для большинства программ в этой книги мы не указали имена файлов, предоставив сделать это вам. Программа начинается со следующих строк: /» Это простая программа, написанная на С#. Файл, содержащий код программы, •• Example.cs. */ Эти строки являются комментарием. С#, как и многие другие языки программиро- вания, позволяет вводить комментарии в исходный код программы, при этом компилятор игнорирует содержимое комментария. В комментарии, адресованном тому, кто читает исходный код, описывается или объясняется операция, выполняемая в программе. В данном случае комментарий описывает программу и напоминает, что исходный файл должен быть назван Example.cs. В приложениях комментарии обычно объясняют, как работают определенные части программы или какие данные содержатся в какой-либо переменной. В C# поддерживаются три типа комментариев. Первый, показанный в самом начале программы, называется многострочным комментарием (то есть может состоять из не- скольких строк). Такой тип комментария должен находиться между символами /* и */. Весь текст, находящийся между этими символами, игнорируется компилятором. Следующая строка программы using System; указывает, что программа использует пространство имен System. Подробнее о пространстве имен мы расскажем далее, а сейчас коротко заметим, что пространство имен обеспечивает способ хранения одного набора имен отдельно от другого. По существу, имена, объявленные в одном пространстве имен, не конфликтуют с такими же именами, объявленными в другом пространстве имен. Итак, данная программа использует пространство имен System, зарезервированное пространством имен для
Первая простая программа 35 элементов, которые ассоциированы с библиотекой классов .NET Framework, исполь- зуемых С#. Ключевое слово using означает, что программа использует имена в заданном пространстве имен. Рассмотрим следующую строку программы: class Example { В этой строке ключевое слово class указывает на то, что объявляется новый класс. Уже говорилось, что класс является базовым элементом инкапсуляции в С#. Exam- ple — это имя класса. Определение класса начинается с открывающей фигурной скобки ({) и заканчивается закрывающей фигурной скобкой (}). Элементы, находя- щиеся между двумя скобками, называются членами класса. На этом этапе не слишком обращайте внимание на детали класса, учтите только, что в C# все процессы программы происходят в пределах класса. Это один из признаков объектно-ориен- тированных программ, какими в той или иной мере являются все Сопрограммы. Далее следует однострочный комментарий'. /! Сопрограмма начинается с вызова метода Main(). Это второй тип комментариев, которые поддерживаются С#. Он начинается с символа // и заканчивается с окончанием строки. Принято общее правило: многострочные комментарии программист использует для больших заметок, а однострочные — для коротких объяснений, не требующих много места. Следующая строка кода начинает определение метода Main (): public static void Main() { Как уже отмечалось, подпрограмма в C# называется методом. Все приложения на C# начинают выполняться с вызова метода Main о. (Так же как программы на С/С++ начинают выполнение с функции main().) Полное объяснение роли каждой части этой строки мы сейчас дать не сможем, поскольку для этого необходимо понимание тонкостей нескольких других свойств С#, но коротко рассмотрим эту строку, по- скольку она будет использоваться в большинстве программ данной книги. Ключевое слово public является модификатором (или спецификатором доступа). Модификатор определяет, как другие части программы могут осуществлять доступ к члену класса. Если перед членом класса находится ключевое слово public, доступ к этому члену имеет код, определенный вне класса, в котором объявлен данный член. (Назначение, противоположное модификатору public, имеет модификатор pri- vate, ограждающий член класса от прямого использования кодом, который опреде- лен вне его класса.) В этой программе метод Main () объявлен как public, поскольку при старте программы он будет вызван кодом, определенным вне его класса (вданном случае непосредственно операционной системой). Примечание В то время, когда писалась эта книга, в C# фактически нс требовалось объявлять метод Main{) как public, но он объявлен таким способом во многих примерах, приведенных в Visual Studio.NET. Кроме того, так предпочитают действовать многие С#-программисты. Поэтому в данной книге мы использовали этот способ, но нс удивляйтесь, если в других книгах вам встретится иное объявление метода Main ().
36 Глава 1. Основы языка СИ Ключевое слово static позволяет вызывать метод Main () до того, как будет создан объект его класса, а поскольку метод Main () вызывается при старте программы, то он обязательно должен быть статическим. Следующее ключевое слово void просто сообщает компилятору, что метод Main () не возвращает значение. (Далее вы увидите, что методы могут также возвращать значения.) Пустые круглые скобки, которые стоят после слова Main, указывают, что методу Main () нс передается никакая информация. (Далее вы увидите, что существует возможность передавать информацию методу Main () или любому другому методу.) Последний символ в строке, символ открываю- щей фигурной скобки ({), указывает начало кода метода Main (). Весь код, составляющий метод, помещается между открывающей и закрывающей фигурными скобками. Приводимый ниже оператор находится внутри метода Main о . Console.WriteLine("Простая Сопрограмма. ") ; Данный оператор выводит строку "Простая Сопрограмма. ", затем отображается новая пустая строка. Фактически вывод выполняется встроенным методом Write- Line (), а информация, которая передается методу (в данном случае это строка "Простая сопрограмма."), называется аргументом. (В дополнение к строкам метод WriteLine () также может использовать другие типы информации для вывода на экран, которые будут рассмотрены далее.) Строка начинается со сл'ова Console, которое является именем встроенного класса, поддерживающего выполнение опера- ции ввода/вывода данных. Записывая имя класса Console и имя метода WriteLine () через точку, вы сообщаете компилятору, что метод WriteLine () является членом класса Console. Использование класса для определения ввода с клавиатуры является еще одним признаком объектной ориентированности языка С#. Отметим, что оператор WriteLine () ; заканчивается символом точки с запятой, как и оператор using System;, указанный ранее в программе. В C# все операторы заканчиваются этим символом. В нашей программе строки, не заканчивающиеся символом точки с запятой, технически не являются операторами. Первая закрывающая фигурная скобка в программе завершает метод Main о, а последняя завершает определение класса Example. Ответы профессионала - — -—-—- •—- > - Вопрос. Вы говорили, что C# поддерживает три типа комментариев, но упомянули только два. Какой же третий тип? Ответ. Третьим типом комментариев в C# является ХМL-комментарий. В нем используются теги XML для поддержки возможности создания самодоку.ментирован- ного кода. Учтите еще, что в языке C# различаются символы нижнего и верхнего регистров, поэтому ошибка в регистре символа может привести к серьезным проблемам. Например, при вводе main вместо Main или writeline вместо WriteLine программа не будет работать. Более того, хотя классы, не содержащие метод Main(), скомпилируются, они будут выполнены после их инициализации в другом классе, содержащем этот метод. То есть если вы введете слово main с маленькой буквы, компилятор все равно скомпилирует вашу программу, но будет выведено сообщение об ошибке, в котором говорится, что программа Example.exe не имеет определенной точки входа.
Небольшое изменение программы 37 Минутный практикум 1. С какого метода начинается выполнение С#-программы? 2. Какие действия выполняет метод Console .WriteLine () ? 3. Как называется программа-компилятор, запускаемая из командной строки9 Обработка синтаксических ошибок Если вы еще этого не сделали, то введите, скомпилируйте и запустите рассмотренную программу. Наверное, вам известно, что при вводе кода можно допустить ошибку. Если введены некорректные данные, компилятор, сделав попытку скомпилировать код программы, сообщит о синтаксической ошибке. Компилятор C# пытается понять ваш исходный код в зависимости от наличия открывающих и закрывающих скобок или признака завершения оператора, поэтому сообщение об ошибке нс всегда отражает истинную причину этой ошибки. Например, если в рассмотренной про- грамме будет пропущена открывающая фигурная скобка после имени метода Main (), то при компилировании предыдущего файла из командной строки будет сгенериро- вана приведенная ниже последовательность сообщений об ошибках. (Подобные сообщения об ошибках также генерируются при компилировании с использованием интегрированной среды разработки IDE.) Example.cs(12,28): error CS1002: ; expected Example.es(13,22): error CS1519: Invalid token '(' in class, struct, or interface member declaration Example.cs(15,1): error CS1022: Type or namespace definition, or end of file expected Очевидно, что первое сообщение об ошибке совершенно ошибочно, поскольку пропущена фигурная скобка, а не точка с запятой. Следующие два сообщения тоже сбивают с толку. Все это мы рассказываем для того, чтобы вы поняли, что не стоит полагаться на информацию, изложенную в сообщении об ошибках, поскольку она может быть неверной. Кроме того, необходимо проверить несколько строк, предшествующих той, номер которой указан в сообщении об ошибке, поскольку иногда сообщение об ошибке появляется только через несколько строк после нее. Небольшое изменение программы Хотя все программы в этой книге используют оператор using System; на самом деле в начале первой программы в нем нет технической необходимости. В C# всегда можно полностью определить имя класса (или метода), указав простран- ство имен, к которому класс (или метод) принадлежит. Например, строка Console .WriteLine ("Простая Сопрограмма . ; 1. Сопрограмма начинает выполняться с вызова метода Main (). 2. Метод Console .WriteLine () выводит информацию на монитор. 3. Программа-компилятор, запускаемая из командной строки, называется csc.exe.
38 Глава 1. Основы языка C# Полное определение метода Console.WriteLine(). ью. может быть переписана следующим образом: System.Console.WriteLine("Простая С#-программа.”); Следовательно, первый пример можно записать так: // В этой версии программы не используется оператор // using System;. class Example { // Сопрограмма начинается с выполнения метода Main(). public static void Main() ( // Здесь метод Console.WriteLine() определен полност System. Console .WriteLine ("Простая Сопрограмма . ") ; <-----‘ Поскольку весьма утомительно каждый раз указывать идентификатор System, когда используется член этого пространства имен, большинство С#-программистов все свои программы начинают с оператора using System;. Но важно понимать, что при необходимости вы можете определить имя класса или член класса явно, указав использование пространства имен, к которому принадлежит данный класс. Вторая простая программа Возможно, оператор присваивания является одним из наиболее важных в программи- ровании. Переменная — это именованная ячейка памяти, которой присваивается значение. И это значение может быть изменено на протяжении выполнения программы. То есть содержимое переменной является изменяемым, а не фиксированным. В следующей программе создаются две переменные, х и у: // Эта программа демонстрирует использование переменных. using System; class Example2 { public static void Main() ( int x; II В этой строке кода объявляется переменная .◄—[Объявление переменных. int у; //В этой строке кода объявляется еще одна переменная. х - 100; // В этой строке кода переменной х-<- // присваивается значение 100. В этой строке кода переменной х присваивается значение 100. Console.WriteLine("Переменная х содержит значение ” + х); У = х / 2; Console .WriteLine (’’Значение переменной у вычисляется"); Console.Write("с помощью выражения х/2 и равно ”); Console.WriteLine(у);
Вторая простая программа 39 Выполнив программу, вы увидите на экране монитора следующее сообщение: Переменная х содержит значение 100. Значение переменной у вычисляется с помощью выражения х/2 и равно 50. В этой программе представлены новые элементы языка С#. Так, оператор int х; // В этой строке кода объявляется переменная. объявляет переменную целочисленного типа с именем х. В C# все переменные должны быть объявлены перед их использованием. Тип значений, которые может хранить переменная должен быть специфицирован. Принято говорить, что указыва- ется тип переменной. В приведенной выше программе переменная х может хранить целочисленные значения. В C# для объявления переменной целочисленного типа перед именем переменной необходимо поместить ключевое слово mt. Следователь- но, оператор int х; объявляет переменную с именем х, имеющую тип з nt. В следующей строке кода объявляется вторая переменная, у: int у; //В этой строке кода объявляется еще одна переменная. Здесь используется тот же формат объявления, который применялся для переменной х, но имя переменной другое. В большинстве случаев для объявления переменной используется оператор с таким синтаксисом: type var-name; Здесь слово type указывает тип объявляемой переменной, a var-name — имя переменной. Кроме типа int в C# поддерживается несколько других типов данных. В следующей строке кода переменной х присваивается значение 100: х = 100; // В этой строке кода переменной х присваивается значение 100. В C# оператор присваивания обозначается символом знака равенства. Он копирует значение, находящееся справа от него, в переменную слева от него. Следующая строка кода выводит значение переменной х, перед которым помешается строка "Переменная х содержит значение ". Console.WriteLine("Переменная х содержит значение " + х) ; При выполнении этой строки кода в результате применения оператора + значение переменной х будет выведено на экран после строки "Переменная х содержит значение ". Это значит, что, используя оператор +, можно в пределах одного оператора WriteLine (); связать вместе сколько угодно элементов. В следующей строке кода переменной у присваивается значение переменной х, деленное на 2. у = х/2; То есть значение переменной х делится на 2 и результат помешается в переменную у. Таким образом, после выполнения оператора переменная у будет иметь значение 50. Значение переменной х останется неизменным. Как и многие другие языки програм- мирования, C# поддерживает полный набор арифметических операторов: + Сложение — Вычитание * Умножение / Деление
40 Глава 1. Основы языка C# Рассмотрим следующие три строки кода программы: Console.WriteLine("Значение переменной у вычисляется Console.Write("с помощью выражения х/2 и равно Console.WriteLine(у) ; Здесь встречаются новый элемент языка и новый вариант использования переменной. Новый элемент — встроенный метод write (), который используется для вывода на экран строки "с помощью выражения х/2 и равно". За этой строкой не следует новая строка. Это означает, что когда будет сгенерирован следующий вывод, он будет помещен в конец этой же строки. Метол Write () похож на метод WriteLine (). за исключением того, что он не выводит новую строку после каждого вызова. Новый вариант применения переменной — самостоятельное использование пере- менной у в вызове метода WriteLine (). В C# для вывода значений любых встроен- ных типов может использоваться как метод Write (), так и метод WriteLine (). Теперь рассмотрим еще одну возможность объявления переменных. В C# можно объявлять две переменные и более, используя один и тот же оператор объявления. Для этого нужно просто разделить имена переменных запятыми. Например, пере- менные х и у можно объявить таким образом: int х( у; // Для объявления обеих переменных // используется один оператор. Другие типы данных В предыдущей программе была использована переменная типа int, которая может хранить только целочисленные значения и нс может быть использована для хранения числа с дробной частью. Ранее мы уже говорили, что кроме типа int C# поддерживает несколько других типов данных. Для чисел с дробной частью в C# определены два типа, float и double, которые представляют числа с обычной и двойной точностью соответственно. Чаще всего используется тип double. Для объявления переменных типа double используется оператор, синтаксис которого представлен ниже: double result; Здесь слово result — это имя переменной, имеющей тип double, позволяющий работать с числами с плавающей точкой (например, она может хранить значение 122.23, 0.034, или -I9.0). Чтобы лучше понять различие между типами int и double, выполните следующую программу: В этой программе демонстрируется различие между типами, int и double. using System; class Example! { public static void Main() {
Другие типы данных 41 inn ivar; // В этой строке кода объявляется переменная типа inc. double dvar; // В этой строке кода объявляется переменная типа double, ivar - 100; // Переменной ivar присваивается значение 100. dvar = 100.0; // Переменной dvar присваивается значение 100.0. Console.WriteLine("Первоначальное значение переменной ivar: ” + ivar); Console. WriteLine (’’Первоначальное значение переменной dvar: " 4 dvar) ; Console. WriteLine () ; // Выводит пустую строку. // Значения обеих переменных делятся на 3. ivar ivar / 3; dvar = dvar / 3.0; Вывод пустой строки Console.WriteLine("Значение переменной ivar после деления: ” + ivar); Console.WriteLine("Значение переменной dvar после деления: " + dvar); } } Ниже показан результат работы этой программы: Первоначальное значение переменной ivar: 100 Первоначальное значение переменной dvar: 100.0 Значение переменной ivar после деления: 33 Значение переменной dvar после деления: 33.3333333333333 Как видите, после деления значения переменной ivar на 3 результатом является целое число 33, то есть дробная часть утрачена. А при делении значения переменной dvar на 3 дробная часть результата сохраняется. Как показано в программе, при необходимости ввода значения с плавающей точкой, она должна быть указана. В противном случае значение будет интерпретировано как целочисленное. Например, в C# значение 100 является целочисленным, а 100.0 — значением с плавающей точкой. Заметьте также, что для вывода на экран пустой строки нужно вызывать метод WriteLine () без аргументов. '^-Ответы профессионала— — - —......................— <gg> Вопрос. Почему в C# для целочисленных значений и значений с плавающей точкой существуют различные типы данных? Ответ. C# поддерживает различные типы данных для повышения эффектив- ности программ. Например, операции над целочисленными значениями производят- ся быстрее, чем над числами с плавающей точкой. Поэтому, если вам нс нужны Дробные значения чисел, то нет необходимости производить вычисления с излишней степенью точности, то есть работать с такими типами данных, как float и double. Кроме того, размер памяти, необходимый для хранения одних типов данных, может быть меньшим, чем размер памяти для хранения других. Предусматривая различные типы данных, C# позволяет эффективно использовать системные ресурсы. Учтите также, что некоторые алгоритмы требуют использования специфических типов данных (по крайней мере, работают при этом более эффективно).
42 Глава 1. Основы языка 0# Проект 1-1. Преобразование значений температуры рТ; Ft0C.CS Хотя предыдущие примеры программ и демонстрируют некоторые важные свойства языка С#, на практике такие программы не слишком полезны. Несмотря на то, что на данном этапе ваши знания языка C# довольно скудные, вы все же можете применить их на практике. В этом проекте мы создадим программу, преобразующую значение температуры по шкале Фаренгейта в значение по шкале Цельсия. В программе объявляются две переменные типа double. В одной и? них будет храниться значение температуры по шкале Фаренгейта, а в другой — значение по шкале Цельсия, полученное после преобразования. Возможно, вы знаете, что для выполнения этого преобразования необходима следующая формула: С = 5/9 * (F ' 32) где С — значение температуры по шкале Цельсия (в градусах), a F — значение температуры по шкале Фаренгейта (в градусах). Пошаговая инструкция I. Создайте новый С#-файл с именем FtoC.cs. (Если компиляция программы производится не из командной строки, а с помощью Visual C++ IDE, необходимо добавить этот файл к С#-проекту, как было описано ранее.) 2. Введите следующую программу в файл: /» Проект 1-1 Эта программа выполняет преобразование значения температуры по шкале Фаренгейта в значение температуры по шкале Цельсия. Назовите файл FtoC.cs. */ using System; class FtoC { public static void Main() { double f; // Содержит значение температуры по шкале Фаренгейта, double с; // Содержит значение температуры по шкале Цельсия. f = 59.0; // Переменная f получает значение 59 // (градусов по Фаренгейту). // Далее выполняется преобразование имеющегося значения // в значение температуры по шкале Цельсия. с - 5.0 / 9.0 * (f - 32.0); Console.Write(f + ” градусов по шкале Фаренгейта равны ”); Console .WriteLine (с + ” градусам по шкале Цельсия.”)*’ } }
Два управляющих оператора 43 3. Скомпилируйте программу, используя Visual C++ IDE (следуя ранее данным инструкциям), или введите в командной строке следующую команду: C>csc FtoC.cs 4. Запустите программу из Visual C++ IDE или из командной строки, для чего введите после приглашения имя главного класса (содержащего метод main): OFtoC В результате работы программы будет выведена следующая строка: 59 градусов по шкале Фаренгейта равны 15 1'радусам по шкале Цельсия. 5. Программа преобразует единственное значение температуры по шкале Фаренгейта (в градусах) в значение температуры по шкале Цельсия. Изменяя число, присваивае- мое переменной f, вы можете преобразовать любое значение температуры. Минутный практикум • f 1 1- Какое ключевое слово в C# используется для целочисленного типа данных? 2. Что обозначает термин double? 3. Обязательно ли в Сопрограмме использовать оператор using System,-? Два управляющих оператора Внутри метода происходит последовательное выполнение операторов по наира пли- нию сверху вниз (то есть так, как они были записаны в данном блоке кода). Однако можно менять этот порядок выполнения с помощью различных управляющих операторов, поддерживаемых С#. Хотя детально управляющие операторы будут рассмотрены в книге позже, о двух из них мы расскажем в этом разделе, поскольку они используются в дальнейших программах. Оператор if Вы можете избирательно выполнить часть программы с помощью условного опера- тора if. Этот оператор во многом сходен с оператором if любого другого языка. В частности, он синтаксически идентичен оператору if в языках С, C++ и Java. Самая простая его форма представлена ниже: if(условие) оператор; Здесь условие— это булево выражение (имеющее значение true или false). Если условие истинно, то оператор выполняется, если условие ложно, то оператор пропус- кается. Приведем пример использования условного оператора if: if(10 < 11) Console.WriteLine("10 меньше, чем 11"); В этом случае число 10 меньше, чем число 11 (условное выражение истинно), следовательно, оператор WriteLine () ,- будет выполняться. Рассмотрим пример: if(10 < 9) Console.WriteLine("Эта строка не будет выведена на экран"); 1. Ключевым словом для целочисленного типа данных в C# является слово int. 2. double — это ключевое слово для данных с плавающей точкой двойной точности. 3. Нет, но это удобно.
44 Глава 1. Основы языка 0# В этом случае оператор WriteLine() ; вызываться не будет, поскольку число I0 больше числа 9. В C# определен комплект операторов сравнения, которые могут использоваться в условных выражениях. Ниже представлены эти операторы и их значения. Оператор Описание < Меньше чем <= Меньше или равно > Больше чем >= Больше или равно == Равно != Не равно Отметим, что оператор равенства состоит из двух символов знака равенства. Ниже представлена программа, в которой используется оператор if: // В программе демонстрируется использование оператора if. using System; class IfDemo ( public static void Main() { int a, b, c; a = 2; b = 3; if (a < b) Console.WriteLine (’’Значение переменной а меньше, чем" + " значение переменной b.”); // Строка, которая приведена в качестве параметра // в следующем методе WriteLine(), не будет выведена на экран. if (а b) Console. WriteLine ("Эта строка не будет выведена на экран.”); _ , ,, . - ,, --------[Использование оператора if. I Console . Wr 11eLine () ; I..............-—------J c — a - b; // Переменной с присваивается значение -1. Console.WriteLine("Переменная с содержит значение -1."); if(с >- 0) Console.WriteLine ("Значение переменной с - " + "не отрицательное число."); if (с < 0) Console .WriteLine ("Значение переменной с - " 4- ” отрицательное число."); Console.WriteLine(); с - b - а; // Переменной с присваивается значение 1. Console.WriteLine("Переменная с содержит значение 1. " ) ; if (с >= 0) Console. WriteLine ("Значение переменной с ~ " + "не отрицательное число.”); if(с < 0) Console-WriteLine("Значение переменной с - ” + ”отрицательное число.");
Два управляющих оператора 45 При выполнении этой программы на экран были выведены следующие строки: Значение переменной а меньше, чем значение переменной Ь. Переменная с содержит значение -1. Значение переменной с - отрицательное число. Переменная с содержит значение 1. Значение переменной с - не отрицательное число. Обратите внимание, что в строке этой программы int а, Ь, с; объявляются три переменные, которые разделены запятыми. Уже говорилось, что если требуются две или более переменных одного типа, они могут быть объявлены в одном операторе. Для этого нужно разделить имена переменных запятыми. Цикл for Вы можете многократно выполнять какую-либо последовательность кода (блок), определив цикл. В этом разделе мы рассмотрим цикл for. В C# этот цикл работает так же, как и в С, C++ и Java. Самый простой синтаксис цикла for: for(инициализация, условие, итерация) оператор; В общепринятой форме часть цикла, отвечающая за его инициализацию, присваивает переменной цикла некоторое начальное значение. Условие является булевым выра- жением, проверяющим переменную цикла. Если результатом этого теста является значение true, цикл for выполняется далее, если — false, происходит естественное завершение работы цикла. Итерационное выражение определяет, как изменяется контрольная переменная цикла при каждом его выполнении. Далее представлена короткая программа, в которой демонстрируется использование цикла for: // Программа, в которой демонстрируется использование цикла for. using System; class ForDemo ( public static void Main() ( int count; for(count = 0; count < 5; count = count+1) ( int num ~ count +1; Console.WriteLine("Зто " + num + "-й проход цикла."); ) Console.WriteLine("Цикл завершен."); } } Ниже показан результат выполнения этой программы: Это 1-й проход цикла. Это 2-й проход цикла. Это 3-й проход цикла. Это 4-й проход цикла. Это 5-й проход цикла. Цикл завершен.
46 Глава 1. Основы языка C# В этом примере count является переменной цикла. Первоначально в части инициа- лизации цикла for ей задается значение ноль. В начале каждого повторения цикла (включая первое) выполняется проверка условия count < 5. Если условие истинно, то выполняется оператор WriteLine О, а затем инициализируется итерационная часть цикла (то есть переменной count присваивается новое значение, равное count + 1). Этот процесс повторяется до тех пор, пока значение переменной цикла перестанет удовлетворять условию. С этого момента выполнение цикла прекращает- ся. Переменная nun была введена в программу только для того, чтобы в выводимых строках нумерация проходов цикла начиналась не с ноля, а с единицы. В профессионально написанной Ctf-программс вы практически не встретите итера- ционную часть цикла, написанную так, как в предыдущем примере. То есть опера- торы, подобные приведенному ниже, встречаются в цикле for крайне редко. count - count т 1; Причина этого в том, что в C# есть специальный оператор инкремента, который выполняет операцию увеличения первоначального значения переменной на единицу более эффективно. Это оператор, обозначаемый двойным символом знака плюс (++). Итерационная часть приведенного выше цикла for с использованием оператора инкремента записывается следующим образом: count++; А весь цикл for будет выглядеть так: for(count - 0; count < 5; count++) Работать он будет точно так же, как предыдущий. Также в C# имеется оператор декремента, обозначаемый двойным символом знака минус (—). Этот оператор уменьшает свой операнд на единицу. Минутный практикум 1. Как звучит полное название оператора if? 2. К каким операторам относится for? 3. Какие операторы сравнения имеются в С#? Использование блоков кода Еще одним ключевым элементом языка C# является блок кода. Блок кода — это объединение двух или более операторов, заключенное в фигурные скобки. После создания блок кода становится логическим элементом, который можно использовать так же, как одиночный оператор. Блок может использоваться в операторах if и for. Рассмотрим следующий фрагмент кода: if (w < h) { v - w * h; w ~ 0; } 1. if — это условный оператор. 2. for относится к операторам цикла. 3. В C# имеются следующие операторы сравнения: <, <-, >, >=, ==, ! =.
Использование блоков кода 47 Если в данном операторе if значение переменной и меньше значения переменной h, то оба оператора внутри блока будут выполнены. То есть поскольку дна оператора внутри блока формируют логическую единицу, один оператор не может выполняться без выполнения другого. Это означает, что если вам понадобится логическая связь между двумя операторами, вы должны поместить их в блок. Блока кода позволяют реализовать многие алгоритмы так, чтобы они были понятны и выполнялись с высокой эффективностью. Ниже представлена программа, в которой блок кода используется для исключения возможности деления на ноль: // В программе демонстрируется использование блока кода. using System; class BlockDemo ( public static void Main() ( int i, j, d; i = 5; j - 10; // Этот блок кода относится к оператору if. if(i ?= 0) { --------------- Console. WriteLine ("Значение переменной i не равно нолю.’’) d=j/i; Console.WriteLine("j/1 равно ” + d) ; Весь 3ior блок кода относится к оператору it. профессионала~ - Вопрос. Влияет ли использование блока кода на эффективность программы (то есть не увеличивается ли время ее выполнения)? Ответ. Использование блоков кода не увеличивает время выполнения про- граммы. Поскольку они способны упростить кодирование определенных алгоритмов, их использование, как правило, увеличивает скорость работы программы и ее эффективность. Результат выполнения этой программы представлен ниже: Значение переменной i не равно нолю, j/i равно 2 В данной программе к оператору if относится блок кода, а не одиночный оператор. Если условие, управляющее оператором if, истинно (как в данном случае), то выполняются все три оператора в блоке. Попробуйте присвоить переменной г значение 0 и посмотрите, каков будет результат. Как вы увидите далее, блоки кода имеют дополнительные свойства и способы применения, но основное их предназначение — создание логически неразделимой единицы кода.
48 Глава 1. Основы языка C# Символ точки с запятой и позиционирование В C# символ точки с запятой означает окончание оператора. То есть каждый отдельный оператор должен заканчиваться точкой с запятой. Как вы знаете, блок является набором логически соединенных операторов, заклю- ченных в фигурные скобки, поэтому его завершает не точка с запятой, а закрывающая фигурная скобка. C# не распознает окончание строки как окончание оператора, только точка с запятой может завершить оператор. Поэтому не имеет значения, в каком месте строки вы помешаете точку с запятой. Например, для C# строки кода х = у,- у - у + 1; Console.WriteLine(х + ” ” т у); будут означать то же, что и строка х = у; у = у + 1; Console.WriteLine(х + ” ” + у); Более того, отдельные элементы оператора также могут быть помещены в разные строки. Так, допустимо следующее расположение: Console.WriteLine("Эта строка при выводе займет довольно много \п" + "места, потому что кроме данных *' + x + y + z + "\п она состоит из трех строковых литералов.”); Такой способ разбивки длинных строк используется для улучшения читабельности программ. К тому же можно увидеть, как эта строка будет выглядеть при выводе. Использование отступов Вероятно, вы заметили, что в предыдущих примерах некоторые операторы написаны с отступом. C# является языком, в котором применение различных отступов или их отсутствие при вводе кода (при вводе операторов) не оказывает влияния на работо- способность программы. Но поскольку существует общепринятый стиль написания программ, которому следует данная книга, советуем вам его придерживаться. Для этого нужно при вводе кода делать отступ (два-три пробела относительно начала верхней строки) после каждой открывающей скобки и возвращаться обратно после каждой закрывающей скобки. Исключение составляют операторы, для которых предусмотрен дополнительный отступ; они будут перечислены позже. Минутный практикум 1. Как создать блок кода? 2. Как завершаются операторы в С#? 3. Справедливо ли утверждение, что все С#-операторы должны начинаться и заканчиваться на одной строке? 1. Последовательность операторов помещается между открывающей ({) и закрывающей (}) фигурными скобками. Блок создает логическую единицу кода. 2. Операторы в C# завершаются символом точки с запятой. 3. Нет.
Использование отступов 49 Проект 1-2. Усовершенствование программы по преобразованию значения температуры FtoCTable.cs Используя цикл for и оператор if, мы усовершенствуем разработанную в первом проекте программу, преобразующую значение температуры по шкале Фаренгейта в значение по шкале Цельсия. В новой версии на экран выводится таблица преобразо- ванных значений, начиная с 0 градусов по шкале Фаренгейта и заканчивая 99 градусами. После вывода десяти строке исходными и преобразованными данными выводится пустая строка. Это выполняется с помошью переменной counter, используемой для подсчета выведенных строк (такой алгоритм называется использованием счетчика). Пошаговая инструкция 1. Создайте новый файл с именем FtoCTable.cs. 2. Введите следующую программу: /* Проект 1-2 В этой программе выводится таблица значений температуры по шкале Фаренгейта и соответствующих им значений температуры по шкале Цельсия. Сохраните эту программу в файле FtoCTable.cs, */ using System; class FtoCTable ( public static void Main() { double f, c; int counter; counter = C;-<____________|Счетчику строк присваивается первоначальное значение О, for(f = 0.0; f < 100.0; f++) ( // Преобразование в градусы по шкале Цельсия, с = 5.0 / 9.0 * (f - 32.0); Console-WriteLine(f + "( по шкале Фаренгейта равны " г с к ”( по шкале Цельсия."); counter =0; // Для отсчета очередных 10 строк. ) 3. Скомпилируйте программу в Visual C++ IDE или введите в командной строке следующую команду: Ocsc FtoCTable.cs
50 Глава 1. Основы языка C# 4. Запустите программу, используя Visual C++ IDE, или введите в командной строке имя программы: OFtoCTable Ниже представлена часть результата работы программы FtoCr Fable. 0 е по шкале Фаренгейта равны -17.7777777777778° по шкале Цельсия - 1° по шкале Фаренгейта равны -17.2222222222222° по шкале Цельсия. 2° по шкале Фаренгейта равны -16.6666666666667° по шкале Лель сия. 3° по шкале Фаренгейта равны -16.1111111111111° по шкале Цельсия. 4 ° по 5° по шкале Фаренгейта шкале Фаренгейта равны -15.5555555555556° по шкале равны -15° по шкале Цельсия. Цельсия. 6° по шкале Фаренгейта равны -14.4444444444444° по шкале Цельсия. 7° по шкале Фаренгейта равны -13.8888888888889° по шкале Цельсия. 8” по шкале Фаренгейта равны -13.3333333333333° по шкале Цельсия. 9° по шкале Фаренгейта равны -12.7777777777778° по шкале Цельсия. 10° по шкале Фаренгейта равны -12.222222222222° по шкале Цельсия. 11 ° по шкале Фаренгейта равны -11.666666666667° по шкале Цельсия. 12° по шкале Фаренгейта равны -11.111111111111° по шкале Цельсия. 13° по 14 ° по шкале Фаренгейта шкале Фаренгейта равны -10.555555555556° по шкале равны -10° по шкале Цельсия. Цельсия. 15° по шкале Фаренгейта равны -9.4444444444444° по шкале Цельсия. 16° по шкале Фаренгейта равны -8.8888888888889° по шкале Цельсия. 17° по шкале Фаренгейта равны -8.3333333333333° по шкале Цельсия. 18° по шкале Фаренгейта равны -7.7777777777778° по шкале Цельсия. 19° по шкале Фаренгейта равны -7.2222222222222° по шкале Ключевые слова в языке C# Цельсия. В языке C# определены 77 ключевых слов (табл. 1.1), которые нельзя использовать в качестве имен для переменных, классов и методов. Таблица 1.1. Ключевые слова C# abstract as base bool breack byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock Long namespace new null object operator out override params private protected publick readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe void ushort while using vortual volatile Идентификаторы Идентификаторами в C# являются имена методов, переменных или других опреде- ленных пользователем элементов. Идентификаторы могут состоять из одного или
Библиотека классов C# 51 нескольких символов. Имена переменных могут начинаться с любой буквы или знака подчеркивания. Вторым символом идентификатора может быть буква, знак подчер- кивания или цифра. Знаки подчеркивания используются для улучшения читабель- ности имени переменной (например, 1 ine__count). Компилятор различает символы верхнего и нижнего регистров (то есть rriyvar и MyVar — разные имена). Ниже приведено несколько примеров допустимых идентификаторов. Test х у2 MaxLoad up _top my_var sample23 Запомните, что идентификатор не может начинаться е цифры. Следовательно, 12.-< не является действительным именем идентификатора. Хороший стиль программиро- вания предполагает использование имен идентификаторов, которые отражаю! зна- чение или особенности применения данного элемента. Хотя в C# в качестве имени идентификатора нельзя использовать ключевое слово, перед ним можно ставить символ @, что позволит использовать ключевое слово в качестве действительного идентификатора. Например, @for является действитель- ным идентификатором. Фактически в этом случае идентификатором является for, а символ @ будет игнорироваться. Использование ключевых слов с начальным симво- лом @ не рекомендуется, за исключением тех случаев, когда это необходимо для специальных целей. Символ @ также может стоять в начале любого имени иденти- фикатора, но использовать его без особой необходимости не рекомендуется. ® Минутный практикум 1. Какое из трех слов является ключевым — for, For или FOR? 3 2, Какие символы может содержать идентификатор в языке С#? salted 3. Являются ли идентификаторы index21 и Index21 одинаковыми? Библиотека классов C# В программах, представленных в этой главе, были использованы два встроенных метода, WriteLine () и Write (). Как уже говорилось, эти методы являются членами класса Console, принадлежащего пространству имен System, которое в свою очередь определено в библиотеке классов. NET Framework. Библиотека классов.КЕТ Frame- work используется в C# для предоставления поддержки операций ввода/вывода, обработки строк, работы в сети и создания интерфейсов GI I. C# тесно ин тегрирован со стандартными классами платформы .NET. Как вы увидите, библиотека классов обеспечивает большинство функциональных возможностей, использующихся в любой Сопрограмме. Чтобы стать программистом на С#, необходимо изучить эти стандарт- ные классы. В этой книге описаны различные методы и элементы библиотеки классов .NET, но поскольку библиотека довольно большая, многие ее компоненты вы должны будете изучить самостоятельно. 1. Ключевым является слово for. В C# все ключевые слова следует писать только строчными буквами. 2. Идентификатор может содержать буквы, цифры и знаки подчеркивания. 3. Нет, в C# учитывается регистр символов.
г 52 Глава 1. Основы языка C# ^Контрольные вопросы 1. Что такое MSIL и почему он так важен для С#? ; 2. Что такое не зависящая от языка среда исполнения (CLR)? 3. Каковы три основных принципа объектно-ориентированного програм- ) мирования? ) 4. С какого метода начинается выполнение программ, написанных на С#? 5. Что такое переменная? Что такое пространство имен? s 6. Какое из следующих имен переменных является недействительным в С#? a) count ' б) Scount ; в) count 27 > г) 67count I д) @if ,[ 7. Как создать однострочный комментарий? Как создать многострочный j комментарий? I 8. Запишите синтаксис оператора if. Запишите синтаксис цикла for. ; 9. Как создать блок кода? . 10. Обязательно ли начинать каждую Сопрограмму следующим опсрато- ? ром: using System; s 11. Сила тяжести на поверхности Луны составляет приблизительно 17 про- ; центов от силы тяжести на поверхности Земли. Напишите программу, \ которая бы вычисляла, каким будет ваш вес на Луне. ; 12. Измените проект 1-2 таким образом, чтобы программа выводила табли- | цу преобразования дюймов в метры. Результат должен вычисляться для s множества расстояний, взятых в диапазоне от 0 до 100 дюймов. После j каждых 12 строк программа должна выводить пустую строку. (Один метр < приблизительно равен 39.37 дюйма.)
гмва№ Типы данных и операторы □ Базовые типы данных C# □ Литералы и их использование □ Создание инициализированных переменных □ Область видимости переменных □ Арифметические операторы □ Операторы сравнения и логические операторы □ Оператор присваивания □ Операция приведения типа, явное и неявное преобразование типов данных □ Выражения в C#
54 Глава 2 Типы данных и операторы Типы данных и операторы составляют основу любого языка нрофаммирования Эти элементы определяют возможности языка и категории задач к которым может быть применен этот язык C# поддерживает большое количество типов данных и опера- торов, что делает его удобным для решения многих задач программирования Рассмотрение операторов и типов данных требует довольно много времени В этой главе мы поговорим об основных типах данных и наиболее часто используемых операторах, а также подробно расскажем о переменных и выражениях Строгий контроль типов данных в C# C# — язык со строгим контролем типов данных Это означает, что все операции в C# контролируются компилятором на предмет совместимости типов данных а сели операция является недопустимой, то она не будет компилироваться Такая строгая проверка типов данных помогает предотвратить ошибки и повышает надежность Для возможности осуществления этого контроля всем переменным, результатам вычис- лении выражении и значениям задан определенный тип (то есть не существует переменной с неопределенным типом) Более того, гип значения опредс гяст виды операции, которые разрешено производить над ним Операция разрешенная тля одного типа данных, может быть запрещена для другого типа Обычные типы данных C# включает две основные категории встроенных типов данных обычные типы (или простые типы) и ссылочные типы К ссылочным типам относятся классы, о которых мы поговорим позже Обычные типы, которых в ядре C# тринадцать, представлены в табл 2 1 Таблица 21 Обычные типы данных Тип Описание Bool Значения true/false (истина/ложь) Byte Char 8 битовое беззнаковое целое Символ Decimal Числовой тип для финансовых вычислений Double Число двойной точности с плавающей точкой Float Число одинарной точности с плавающей точкой Int Целое число Long Sbyte Short Длинное целое 8 битовое знаковое целое Короткое целое Uint Беззнаковое целое Ulong Ushort Беззнаковое длинное целое Беззнаковое короткое целое В C# для каждого типа данных строго специфицированы диапазон значении и возможные операции над ними Соответственно требованиям переносимости в C#
Обычные типы данных 55 строго контролируется выполнение этих спецификаций. Например, тип данных int одинаков для любой среды выполнения программ, поэтому нет необходимости переписывать код для обеспечения совместимости этого типа с каждой отдельной платформой. Строгая спецификация необходима для достижения переносимости, хотя на некоторых платформах может привести к небольшой noicpe производитель- ности. Целочисленные типы В C# определены девять целочисленных типов данных: char, tyte, sbyte. chart, ushort, int, tint, long и ulong. Тип данных char в основном применяется для представления символов и будет описан позже в этой главе. Остальные восемь типов используются для выполнения операций с числами. В следующей таблице перечис- лены эти типы, а также указано количество битов, выделяемых для представления числа, и диапазон возможных значений. Тип Количество битов Диапазон значений byte 8 От 0 до 255 sbyte 8 От-128 до 127 short 16 От -32768 до 32767 ushort 16 От 0 до 65535 int 32 От -2147483648 до 2147483647 uint 32 От 0 до 4294967295 long 64 От -9223372036854775808 до 9223372036854775807 ulong 64 От 0 до 18446774073709551615 Как показано в таблице, в C# определены как знаковые, так и беззнаковые варианты целочисленных типов. Различие между ними состоит в способе интерпретации старшего бита целого числа. Если указывается знаковое целое, то компилятор будет генерировать код, в котором предполагается интерпретация старшего бита целого числа как флага знака. Если флаг знака 0, то это положительное число; если флаг знака I, то число отрицательное. Отрицательные числа практически всегда представ- лены с использованием метода двоичного дополнения. В этом методе все биты числа (за исключением флага знака) инвертируются, затем к этому числу прибавляется единица, в завершение флагу знака задается значение I. Знаковые целые важны для очень многих алгоритмов, но они имеют только половину абсолютной величины соответствующих беззнаковых целых. Например, запишем в Двоичном представлении число типа short 32767: 01111111 шиш Поскольку это знаковый тип, при задании старшему биту значения 1 число будс, интерпретировано как -1 (если использовать метод двоичного дополнения). Но сели вы объявляете его тип как eshort, то при задании старшему биту значения 1 число будет интерпретировано как 65535. Пожалуй, наиболее часто используемым целочисленным типом является int. Пере- менные типа int часто применяют для управления циклами, для индексации массивов и в различных математических вычислениях. При работе с целочисленными значениями, большими, чем те, которые могут быть представлены типом int, в C# используют типы uint, long, ulong, а при работе с беззнаковым целым — тип uint.
56 Глава 2. Типы данных и операторы Для больших значений со знаком применяют тип long, для еще больших положи- тельных чисел (беззнаковых целых) — тип ulong. Ниже приведена программа, вычисляющая объем куба (в кубических дюймах), длина стороны которого равна I миле. Поскольку это значение довольно большое, для его хранения программа использует переменную типа long. /* Программа вычисляет объем куба (в кубических дюймах), длина стороны которого равна 1 миле. (Для справки: в одной миле 5280 футов, в одном футе 12 дюймов. */ using System; class Inches { public static void Main() { Long ci; Long im; im = 5280 * 12; ci = im * im * im; Console.WriteLine("Объем куба с длиной стороны, равной 1 миле, "< "\правен "+ ci +" кубических дюймов."); } } Ниже показан результат выполнения этой программы: Объем куба с длиной стороны, равной 1 миле, равен 254358061056000 кубических дюймов. Очевидно, что такой результат не может быть помещен в переменную типа int или uint. byte и sbyte — наименьшие целочисленные типы. Значение типа byte может находиться в диапазоне от 0 до 255. Переменные типа byte особенно полезны при использовании необработанных двоичных данных, таких как поток байтов данных, сгенерированный каким-либо устройством. Для небольших знаковых целых приме- няется тип sbyte. Представленная ниже программа использует переменную типа byte для контроля цикла for, в котором вычисляется сумма всех целых чисел, находящихся в диапазоне от 1 до 100. // Использование типа byte. using System; class Use_byte ( public static void Main() { byte x; int sum; sum 0; for(x - 1; X <= 100; x+ + ) sum — sum x;
Обычные типы данных 57 Console.WriteLine("Сумма всех целых чисел, находящихся "в диапазоне оз 1 до ICC, Хправна " - sec); } } Результат выполнения этой программы следующий: Сумма всех целых чисел, находящихся в диапазоне от 1 до 100, равна 5050 Поскольку в данной программе для управления циклом for используются числа от 1 до 100, находящиеся в диапазоне значений, определенных для типа byte, нет необходимости назначать тип переменной, позволяющий работать с гораздо больши- ми числами. Для правильного назначения типа переменной и экономии системных ресурсов воспользуйтесь приведенной выше таблицей и выберите диапазон значений типа, которому удовлетворяет значение этой переменной. Типы данных с плавающей точкой Как уже говорилось в главе I, типы данных с плавающей точкой могут представлять числа, имеющие дробные части. В C# существуют два типа данных с плавающей точкой, float и double, представляющие числа с одинарной и двойной точностью соответственно. Для представления данных типа float выделяются 32 бита, что позволяет присваивать переменным значения чисел, находящихся в диапазоне от 1.5Е-45 до 3.4Е+38. Для представления данных типа double выделяются 64 бита, что расширяет диапазон используемых чисел до величин из диапазона от 5Е-324 до 1.7Е+308. Чаще всего применяется тип double. Одна из причин этого в том, что многие математические функции в библиотеке классов C# (которой является биб- лиотека .NET Framework) используют значения, имеющие тип double. Например, метод Sqrt (), определенный в стандартном классе System.Math, возвращает значение типа double, являющееся квадратным корнем его аргумента, также имеющего тип double. Ниже в качестве примера приведена программа, в которой дтя вычисления длины гипотенузы, заданной длинами двух катетов, используется метод Sqrt (). /* В программе используется теорема Пифагора, позволяющая найти длину гипотенузы по известным длинам катетов. */ using System; class Hypot { public static void Main() { double x, y, z; x - 3; у - 4; z = Math.Sqrt(x*x + y*y); < Обратите внимание на способ вызова метода Sqrt () - Имя метода отделено точкой ог имени класса, членом которого он является. Console.WriteLine("Длина гипотенузы равна " + z) ; } }
58 Глава 2. Типы данных и операторы Результат выполнения этой программы: Длина гипотенузы равна 5 Отмстим еще одну особенность данной программы. Как уже говорилось, метод Sqrtl) является членом класса Math. Обратите внимание на способ вызова метода Sqrr. () — имя метода отделено точкой от имени класса, членом которого он является. Похожий способ записи нам уже встречался, когда перед именем метода Write- Line () стояло имя его класса Console. Нс все стандартные методы нужно вызывать, указав вначале имя класса, в котором определен данный метод, ио некоторые методы требуют именно такого вызова. Тип decimal Возможно, наиболее интересным числовым типом данных в C# является тип deci- mal, предназначенный для использования в денежных вычислениях. В типе decimal для представления значений, находящихся в диапазоне от 1 Е-28 до 7.9Е+28, исполь- зуется 128 битов. Вы, конечно, знаете, что в обычных арифметических вычислениях, производимых над числами с плавающей точкой, неоднократные округления значе- ний приводят к неточному результату. Тип данных decimal устраняет ошибки, возникающие при округлении, и может представлять числа с точностью до 28 деся- тичных разрядов (а в некоторых случаях и до 29 разрядов). Эта способность пред- ставлять десятичные значения без ошибок округления особенно полезна, когда рассчитываются финансы. В качестве примера рассмотрим программу, которая для денежных вычислений использует тип данных decimal. Программа вычисляет баланс после начисления процентов. /* Программа демонстрирует использование типа decimal для финансовых вычислений. */ using System; class UseDecimal { public static void Main() { decimal balance; decimal rate; // Подсчет нового баланса balance = 1000.10m; •<------------- rate = 0.1m; balance = balance * rate + balance; Console. WriteLine ("Новый баланс: $” -r balance); } } Эта программа выводит следующий результат: Новый баланс: $1100.11 При вводе значения типа decimal должны заканчивахься символом гл или М.
Обычные типы данных 59 Ответы профессионала Вопрос. Ранее я работал с языками программирования, в которых не было ^типа данных decimal Является ли он уникальным и присущим только С#9 Ответ. Да, этот тип данных уникальный; такими языками, как С, С+1 2 * 4- или Java, он не поддерживается. Отметим, что после десятичных констант (то есть констант, имеющих тип decimal) должен следовать суффикс m или м, иначе эти значения будут интерпретированы как константы с плавающей точкой, не совместимые с типом данных decimal (Далее в этой главе .мы подробно рассмотрим, как специфицируются числовые константы ) Минутный практикум I. Какие целочисленные типы данных существуют в С#? 2. Назовите два типа данных с плавающей точкой. 3. Почему тип данных decimal так важен для финансовых вычислении9 Символы В отличие от других языков программирования (таких, как C++, в которых для представления символа выделяется 8 битов, что позволяет работать с 255 символами) в C# используется стандартный набор символов Unicode, в котором представлены символы всех языков мира. В C# char — беззнаковый тип, которому ддя представ- ления данных выделено 16 битов, что позволяет работать со значениями, находящи- мися в диапазоне от 0 до 65535 (то есть с 65535-ю символами). Стандартный 8-битовыи набор символов ASCII является составной частью Unicode и находится в диапазоне от Одо 127. Таким образом, ASCII-символы остаются действительными в C# Для того чтобы присвоить значение символьной переменной, нужно заключить в одинарные кавычки символ, стоящий справа от оператора присваивания. Ниже показан синтаксис операторов, при выполнении которых переменной ch присваива- ется значение X. char ch; ch - •х'; Для вывода символьного значения используется оператор WriteLine (). Следующая строка выводит значение переменной ch. Console.WriteLine("Значение переменной ch - " + ch); Тип char, определенный в СТ как целочисленный тип данных, никогда нс может быть свободно совмещен с целочисленными типами, предназначенными для 1- В C# определены следующие целочисленные типы данных: byte, short, int, long, soyte, ushort, uint и ulong. Тип данных char также технически является целочисленным, но в основном он используется для хранения символов. 2- Типы данных с плавающей точкой — float и double 3. Данные типа decimal используются для финансовых вычислений, поскольку они не подвержены ошибкам округления.
60 Глава 2. Типы данных и операторы представления чисел, поскольку отсутствует автоматическое преобразование из це- лочисленного типа в спаг. Например, следующий фрагмент кода недействителен: char ch; ch - 10; // Данный оператор недействителен. Причина этого в том, что значение 10 — целочисленное и нс будет автоматически конвертировано в тип char. Следовательно, операнды данной операции присваива- ния представляют собой несовместимые типы. Если вы попытаетесь скомпилировать этот код, компилятор сообщит об ошибке. Далее в этой главе мы расскажем, как можно обойти это ограничение. qs, Вопрос. Почему в C# используется Unicode? Ответ. В задачи разработчиков C# входило создание такого компьютерного языка, который позволял бы писать программы, предназначенные для использования во всем мире. Поэтому авторы воспользовались стандартным набором символов Unicode, в котором представлены все языки мира. Конечно, использование этого стандарта неэффективно для таких языков, как английский, немецкий, испанский или французский, символы которых могут быть представлены 8 битами, но это цена, которую приходится платить за переносимость программ в глобальном масштабе. Булев тип данных Для булева типа данных, bool, в C# определены два значения true и false (истина и ложь). Следовательно, переменная типа bool или логическое выражение будут иметь одно из этих двух значений. Более того, нс существует способа преобразования значений типа bool в целочисленные значения. Например, значение 1 нс преобра- зуется в значение true, а значение 0 — в значение false. Приведем программу, демонстрирующую использование типа данных bool. // Прох7рамма демонстрирует использование // переменной, имеющей тип boor. using System; class BoolDemo { public static void Main() { bool b; _________________________ Булева переменная может управлять b false* условны м oncpai ором i f Console.WriteLine("Переменная b имеет значение " + b); b - true; Console.WriteLine("Переменная b имеет значение " + b); // Булева переменная может управлять условным оператором if. if (b) Console.WriteLine("Этот оператор будет выполнен."); b = false; if (bi Consol Р Wri tpT т пр i " Черт ппрпд.ппр up Схл <то>п /
Обычные типы данных 61 // Условный операюр возвращае булево значение Console hr xteLire (" (10 > 9)-эю + (д.0 > 91) } } Ниже представлен результат, сгенерированным этой программой Переменная о имеен значение False Переменная Ь имеет значение True Этот оператор буде^ выполнен. (10 > 9) эю True Обратите внимание на некоторые особенности использования булевой переменном Во-первых, при выводе значения типа bool с помошью метода Jr iteLine () на экран выводится слово true или false Во-вторых, испотьзуя переменную типа t ос 1 можно управлять оператором if Ести устопнем выло тения оператора f явтяеня истинность переменной (в данной программе переменной Ь) то нет необходимости записывать оператор xf так lf(b — true) достаточно ботсе короткого выражения if (Ь) В-третьих, результатом выполнения оператора сравнения, такого как с, является значе- ние типа bool Вот почему выражение г0 > 9 выводит на экран значение true Далее, дополнительная пара скобок, в которые заключено выражение 13 > 9. является необхо- димой, поскольку оператор + имеет больший приоритет, чем оператор > G> Минутный практикум 1 Что такое Unicode' 1 J 2 Какие значения может иметь переменная типа bool9 3 Каков размер данных типа char в битах’ Форматирование вывода Прежде чем продолжить рассмотрение типов данных и операторов, сдсчасм неботь- шое отступление До этого момента при выводе списка чанных мы раздетяти чаши списка с помощью знака птюс, как показано ниже Console WriteLine ( 'Было заказано " с z + ' кн/ii- i о ?' т- 3 + 'За единицу товара ’), Это удобно, но такой вывод чистовом информации нс позвотяс! контро шровагь способ появления данной информации на экране Например нельзя контролировать выводимое число десятичных разрядов для значении с птаваюшеи точкой Рассмо!- Рим следующий оператор Console WriteLine ("10/3 - ' + 10 C/3 0), 1 Unicode — это набор международных символов, каждый из которых представлен 16 битами 2. Переменная типа bool может иметь значение либо true, либо false 3 Данные типа char представлены 16 битами
62 Глава 2. Типы данных и операторы После его выполнения на экран будет выведена следующая строка: 10/3 3.33333333333333 Для некоторых целей вывод такого большого числа десятичных разрядов оправдай, но в других случаях он может быть неприемлем. Например, в финансовых вычисле- ниях обычно требуется вывод только двух десятичных разрядов. Для форматирования числовых данных необходимо использовать перегруженный метод WriteLine () (перегрузка методов будет рассматриваться в главе 6), в один из параметров которого можно внедрить форматирующую информацию. Синтаксис этого метода выглядит так: WriteLine("форматирующая строка", argO, argl, ... , argN); В этой версии метода WriteLine () аргументы разделены запятыми, а нс знаками плюс. Форматирующая строка состоит из стандартных выводимых без изменения символов и спецификаторов формата. Синтаксис спецификаторов формата обычно выглядит так: {агдпып, width: fmt} Здесь элемент argnum указывает номер аргумента (начиная с ноля), который должен быть выведен на экран в данном месте строки. Элемент width определяет минималь- ную ширину поля, предоставляемого данному аргументу, а элемент frnt специфици- рует используемый формат вывода. Когда при выполнении в форматирующей строке встречается спецификатор формата, программа заменяет его соответствующим аргументом, который и выводится на экран. Следовательно, позиция спецификатора формата в форматирующей строке указывает место, где на экране будут выведены соответствующие данные. Элементы width и fmc являются не обязательными. Следовательно, в своей простейшей форме спецификатор формата указывает аргумент, необходимый для вывода на экран. Например, спецификатор (0) указывает на аргумент argO, спецификатор (1} указывает на аргумент argl и так далее. Теперь рассмотрим простой пример. Оператор Console.WriteLine("В феврале {0} или {1} дней.", 28, 29); выведет на экран следующую строку: В феврале 28 или 29 дней. Как видите, спецификатор (01 заменяется значением 28, а спецификатор (I1 — значением 29. Обратите внимание, что аргументы метода WriteLine О разделяются запятыми, а не знаками плюс. Следующий пример демонстрирует усовершенствованную версию предыдущего опе- ратора, в которой специфицирована минимальная ширина поля: Console.WriteLine("В феврале {0,10} или {1,5} дней.", 28, 29); Результат выполнения данного оператора следующий: В феврале 28 или 29 дней. Если выводимое число меньше заданной ширины поля, то неиспользованные части поля заполняются пробелами. Помните, что ширина поля — это минимальное количество позиций, выделенных для выводимого значения. Если для вывода числа потребуется большее количество позиций, они будут выделены автоматически.
Обычные типы данных 63 В предыдущих примерах к самим значениям форматирование нс применялось, но безусловно, используемые спецификаторы формата должны контролировать отобра- жение данных. Чаше всего форматируются значения с плавающей точкой и значения, имеющие тип decimal. Одним из простейших способов спецификации формат является написание шаблона, который будет использован методом WriteLine() при форматировании выводимых значений. Шаблон создается следующим образом: в спе- цификаторе формата указывается номер аргумента, затем после двоеточия при помощи символа # указываются позиции выводимых цифр. Так, после выполнения оператора Console.WriteLine("10/3 - {О:#.##}”, 10.0/3.0); программа выведет отформатированное значение, являющееся результатом деления 10.0 на 3.0, Ю/з = з.зз В этом примере шаблон создается последовательностью символов 0 . ##, с помощью которых методу WriteLine () сообщается о необходимости вывода двух десятичных разрядов. Если программой выводится число, у которого целая часть содержит более одного знака, то во избежание потери данных метод WriteLine О будет выводить более одной цифры слева от запятой. В случае необходимости вывода значения в долларах и центах используйте специфи- катор формата с. Например, decimal balance balance = 12323.09m; Concole-WriteLine("Текущий баланс равен; {0:0}", balance); В результате будет выведено следующее сообщение: Текущий баланс равен: $12,323.09 Проект 2-1. Разговор с Марсом 0 Mars.cs Наименьшее расстояние между Марсом и Землей составляет примерно 34 миллиона миль. Предположим, что на Марсе находится человек, с которым нам необходимо поговорить. Для этого нужно написать программу, которая могла бы вычислить, сколько времени потребуется сигналу на преодоление этого расстояния. Скорость света равна примерно 186000 миль в секунду, следовательно, для вычисления времени задержки сигнала (времени между отправкой и приемом сигнала) необходимо разде- лить расстояние на скорость света. Значение времени должно быть выведено в секундах и минутах. Пошаговая инструкция 1- Создайте новый файл и назовите его Mars . cs. 2- Поскольку значение времени задержки сигнала будет содержать дробные части, для него необходимо использовать значения с плавающей точкой. Перечислим переменные, которые будут использоваться в программе: double distance; double lightspeed;
64 Глава 2. Типы данных и операторы douole delay; double deiay_in_m.n; 3. Задайте переменным distance и lightspeed начальные значения: discance = 34000000; // 34000000 миль lightspeed = 186000; // 186000 миль в секунду 4. Для вычисления времени задержки сигнала разделите значение переменной distance на значение переменной lightspeed. При этом будет получено значе- ние задержки сигнала в секундах. Присвойте это значение переменной delay и выведите результаты следующим образом: delay = distance / Lightspeed; Console.WriteLine (’’Расстояние между Землей и Марсом сигнал ” + ’’пройдет за \п" 4 delay + " секунд."); 5. Для получения значения результата в минутах разделите значение результата в секундах, который содержится в переменной delay, на 60; выведите результат на экран, используя следующие строки кода: delay_in_min = delay / 60; Console. WriteLine ("Время задержки сигнала составляет " н aelay_in_irin -г " минут . " ) ; Ниже приведен полный листинг программы Mars.cs’ /’ Проект 2-1 "Разговор с Марсом" Назовите этот файл Mars.cs */ using System; class Mars { public static void Main() { double distance; double lightspeed; double delay; douole delay_in_min; distance - 34000000; // 34000000 миль lightspeed - 186000; If 186,000 mhj1O в секунду delay - distance / lightspeed; Console .WriteLine ("Расстояние между Землей и Марсом сиг нал ’’ 4 "пройдет за \n" + delay + " секунд."); delay_in_min = delay / 60; Console.WriteLine("Время задержки сигнала составляет " + delay_in_min + " минут."); } }
Литералы___________________ 65 6 Скомпи щрхитс it защитите npoipaxixix Резх и iai ее выпо 1нсния ox lei е ie ixioimixi Расс ояг .'с ио.л, '' с ь - I J Время ад у w а а я тг 7 Конечно бО1ЬШИИЫВО 1ЮДСИ В CBOUI р ЮОК нс u i imihhhi с hiliimi 1\1С О Ш.ИМИ СТИШКОМ МНОГО LLCH1II |НЫ\ рнряюв [15 \ 1\ I IJC1U1 1 1 Юс И НОс II В1 ВОДИМОГО pcj>\ 1ЬГ11 1 ПрО1р1ММЫ ПМСН1НС LOO 1 ВС 1 с I В\ Ю1 L11C онер 1 opl I Line О, предо IB ЦННЫМИ НИМ Console <_L r ( I a c ch /•_ м a 1 u/ n P 3 t*rv UJT Console Ax. Ге( a i_A /Г. J 4Г MMbyi e \ 8 Повторно LKOM1IH llip\ П1С 13ill\cll!lC IpOipiMMv lelKOI 1 iJKpillOXle У Bl <11 отформатированной pen н i u bi i i ic сини Расс ояние viex^v м e г e i 182 796 сек/tд Время задержку cniHaja c^ ih че J i-' В ЭТОМ CIVUC И I _>KpiH ВЫВО 1Я1СЯ IO 11 KO pH ICC Illi nil l\p >p51Ll Литералы В C# tumepaia\iu (in i / онстанть ми) и инн пог фш сиров hihi ic и пения пре ict из ленные в у лобной для пения форме (например пню !()() яв1яс1ся шер ом) В ОСНОВНОМ ТШСраДЫ 11 СПОСОбЫ ИХ 11СПО И _>О1 1Н11 I II 1С О 11 КО ИОН 11111 ПО МН ( I особых объяснении примени 1И их в гои и ш иной форме ВО Всех пре II I IX ПИХ Ipoi р 1М мах Теперь же р1сскгжсм о них оо ice по грооио В C# литералы могут оьнь значениями iioooio пни oi коюрою зп icii спосоо представления каждою in icp.i ) К1кхжс оворп юсь с ихпо иные коне i пн ы з н ю чаются В ОДИН грпыс К IBI I IKII 1 IK ll ПИЯКИО cIIXIBO I НИМИ 1 nc H IM I Целочис icHHbic нпсра 1Ы сне цнфпцпрх io re я к IK ни i ion ipooiion iichi lluipii'iip 10 и -100 я в оно гея tic io Hie re Hill iM i коне 1 in i ми Копс нити с пив ионкп го i коп требуют при написании йеной юв гния кся1 и пюи io 1ки з i ко opoiicicixci гроон 1я часть чиста Н uipiiMcp II 123 hbihctcM kohcIihioii с iiiniionicn никои В С разрешается тткже испо п зов н ь гкспоне нии 1 и нос пр iciiiicnuc пинен ш но Щей точкой Поско ТЬКХ C# — Я31 IK СО С I рогпм КОН ipo 1С М 1ИНОВ чпереш I 1КЖС о ipc К ПО сЯ как прин 11 гежапгис к к ikomx иное п гинх Ч оо i нс uhiiikiio iiipx ihch ш при Определении Ilina К1ЖЮ1О inc iobo О lllcpi I ( eX lecll lOI LU ll Lil line Лрави la Типом Нс го nic ichhi ix hi icp i iob ян inc ini и iiimc hi шин о ) ic c i и in i ип н i i in in c Шт который способен хранин ПННОС IIIC IO C Ic говне ll HO I iihiiciimi CI 11 OI величины чис г i гипохг цс ю гис ichhhx пнереюн xioxci они not ulorg Лшералы с плавающем го гкои iimcioi тип c Если вас нс устраивает тип опрсдс генный в C# по умо i ianino вы можете знать Нужный тип лшерала явно посредством добавления суффикса Чтобы хкыагь гип
66 Глава 2 Типы данных и операторы литерала long, добавыс к числу букву * или 1 (например, число 20 mmcci тип i’--. a I2L - inn long). Для спецификации беззнакового целочисленною литерала добавьте букву i или 1 (например, число 100 имеет тип _nt, a 100L — тип а^гт) Для спецификации длинного беззнакового целою прибавьте к константе суффикс ч1 или UL (например, литерал 984375LL имеет тип cicnq) Чтобы специфицировать литерал типа И оа~, добавьте к консгатс символ f или F (например, литерал 10 19Г- имсет тип float). Для спецификации литералов типа c.eci"'al добавьте к значению букву л или м (9 95М является литералом, имеющим тип ntr-n). Хотя целочисленные литералы по умолчанию создаются как значения, имеющие тип лр unit. long или с.1эга. они могут бьиь присвоены переменным типа byte. soyte, short или г. ,t< rt. но только в гом с ,учае, если присваиваемое значение вообще может представляйся этим типом. Целочисленный литерал всстда може, быть присвоен переменной тина long. Шестнадцатеричные литералы Иногда в программировании вместо десятичной легче применять шестнадцатерич- ную систему, использующую цифры от 0 до 9 и символы от А до F, которым присвоены значения от Юдо 15 соответственно. Например, шееiнадцатеричное число К) соответствует 16 в десятичной системе исчисления Учитывая достаточно частое применение шестнадцатеричных чисел, C# позволяет использование целочисленных констант в шестнадцатеричном формате. Шестнадцатеричные литералы должны начинаться с символов Ох (ноль и х) Приведем несколько примеров count - ОхЬЬ; // 255 в десятичной сис.еме incr — 0x1а; // 26 в десятичной системе Символьные escape-последовательности В C# большинство выводимых символьных консгант можно заключать в одинарные кавычки, но применение некоторых символов (например, символа возврата каретки) является проблематичным Кроме того, часть символов (например, символы оди- нарных или двойных кавычек) имеют в C# специальное значение, поэтому их прямое использование недопустимо. По этим причинам в C# предусмотрены специальные сьсарс-последоватеимоспш, коюрые перечислены в табл. 2.2 Эти последовательности используются вместо символов, которые они представляю! Например, таким образом переменной . присваивается символ табуляции ch - '\t'; Атак переменной ст присваиваюк'я одинарные кавычки ch ' \ ; Таблица 2 1 Символьные escape-последовательности Escape-последовательность Описание \а Предупреждение (звонок) \Ь Возврат на одну позицию (backspace) \1 Переход на новую страницу (formfeed) \п Переход на новую строку (linefeed) \г Возврат каретки
Литералы 67 Escape-последовательность Описание Горизонтальная табуляция Вертикальная табуляция Ноль \t w \0 одинарные кавычки Двойные кавычки Символ обратной косой черты Строковые литералы В C# поддерживаегея еще один гип штора юв — строка, являющаяся наоором символов, заключенных в нюиные кавычки Ниже приведен пример с (роки "Небольшой проверочный ickc ” Вы уже встречали примеры мких строк, применявшиеся во многих операторах WriteLine () , предыдущих программ Кроме обычных сим во юн строковые зитсраты могу г содержать очну и ш веско шко escape-п ослсдо вате тьносте и В качестве примера рассмотрим программу, которая использует escapc-noc ледова те тыюсги ‘ - и х - // В программе демоне ргруе // в строковых литералах использование escape 1 ос 1едова*слэЮч, е/ using System, class StrDemo I public static vo.d La n() I Console WriteLine("Первая Испо «ьзованис esc ipc пос ic iobhc ilhocih i для переход i на новую с (ром 1рока \i Вторая с пока ”) Console Wn.teL.ne ( 'A\cB\ 1C") , Console WriteLine(”D\tE\rF”), Испо (ьзованис табуляции для выр (вникания выводимых СИМВО IOB Эта программа выводит с юдующиг строки Первая сорока Вторая сIрока А в с D Е ь Вопрос Я знаю, что в языке C++ разрешается специфицировать ппсраты в восьмеричной системе Разрешены ди восьмеричные пператы в С#’ Ответ Нет, в C# титераты можно специфицировать тотько в тссятичнои и шестнадцатеричной форме В современном программировании восьмеричная форма встречается редко Заметьте, что cscape-noc юдовагс шноегь испо 1ьзустся для псрсхо ia на новую сроке Чтобы разбить выводимые данные на несколько строк, нс обязатетьно нснотьзоваш
68 Глава 2. Типы данных и операторы множество операторов z'rireLine () ;, достаточно помести 1Ь cscapc-последователь- ность \п в пределах длинной строки там, где должна начинаться новая строка. В дополнение к вышеизложенному добавим, что в C# имеется еще возможность специфицирования копирующегося строкового литерала. Он начинается с символа @, за которым следует строка в кавычках. Содержимое строкового литерала, взятого в кавычки, принимается без модификации и может охватывать две и более строки. Следовательно, в этом случае можно использовать символы перехода на новую строку, табуляции и так далее без применения escape-последовательностей. Единст- венное исключение состоит в том, что при необходимости получения в выводимых данных символа двойных кавычек (") их нужно ввести дважды подряд Следующая программа демонстрирует использование копирующихся строковых литералов. // В программе демонстрируется использование копирующихся строковых литералов. using System; ciass Verbatim { public static void Main () Console.WriteLine(@"Это который охватывает Этот колирующийся строковый литерал содержит вложенные символы перехода на новую строку. копирующийся строковый литерал, несколько строк. ”) ; Console.WriteLine"Здесь также используется табуляция: 12 3 4 -4----|В этом копирующемся строковом литерале содержался также символы табуляции. 5 6 7 8 ”) ; Console.WriteLine(б"Программисты говорят: ""C# - интересный язык."""); } } Результат выполнения этой программы выглядит следующим образом: Это копирующийся строковый литерал, который охватывает несколько строк. Здесь также ИСИОЛ1 ззуется табуляция 1 2 3 4 5 6 7 8 Программисты говорят: "C# - интересный язык." Обратите внимание на то, что копирующиеся строковые литералы выводятся в программе точно так же, как они были введены. Но если копирующийся литерал занимает несколько строк и символы выводятся на экран с самого начала строки, то нарушается читабельность программы, поскольку не соблюдается стиль ввода кода с использованием отступов. Поэтому мы не используем копирующиеся строковые литералы в программах данной книги, но вы должны знать, что это прекрасное решение во многих случаях, когда требуется форматирование.
Переменные и их инициализация 69 Минутный практикум I Каков ши nncpii 1 О’ К ко» hi 2 Спсцифииируигс зн 1 iliihl К ) как и ср । 10 () ( псаифп ЦП lie ЗН I 1С11ИС I (К) к 1К 3 Чю riKoe °Т> профессионала Вопрос Яв1яс1ся in симво и hi гм । nep i юм трок i символ(например и < )’ солсржащщ опто пи in Ответ Her Нс пут шге срок i с символ i\m Симвопны i i Iicpdi прелег in ляетедини тын ciimbo i тип i Строк i содсрж ндая io и ко о ihv б кву napiBiio остается строкой Хотя строки состоял из символов они имени ipxron гип временные и их инициализация О переменных мы уже коротко говорили в лаве 1 Вам извей но то гсрсУ1Снныс объявляются с помощью оперттора имеющие с к тутошни синтаксис type var тагу, где tycp — это тип переменной t - с имя Ви можете обтязлягь переменные тюбого тсиствитсл! ною inni вкпои р осмотренные выше прост 1с типы При создании переменной ашмея экзхмп nip се uni сктовагс i но воз можности переменной опредстяюкз се iniioxt Н шримср перемени 1Я тип 1 нс МОЖСТ ПСПОТЬЗОВ 1ГТСЯ ТЛЯ хрнтсния JH1KIIIIIU II 111Ю1Ц111 OIKOII Ьо ICC IOIO нр I объявлении переменной се тип фиксируйся и изу1снять ио не п зя Н i ipiiMcp переменная тип i не можо прсврнитыя в переменную гип i Все переменные в C# тотжны они ooi ян ины до их нс по и зов ни i Э10 нсобхо шмо поскольку компнтяюру НУЖНО ЗН III II I UHHIIX которые СОЦ-РЖП! перемени ТЯ о того как он н i me i компп тиров m oncpiropii нс по и зу юшнс лу ncpcxiumxio В C# onpciciciio нсскол! ко ришпзнх iii iob перемени i\ Переменные кою| ыс ИСПОЛЬЗОВа 1ИС1 В HILLHIX IipOipiMMlX Ю J1O1O MOMUII I II13IIBI1O СЯ 101(1 I 1 < переменными TOCKO II КУ ОНИ OOI I ’> IJI Hie В 1OIICII1 311 I i Moe 111 XI 011 Инициализация переменной Прежде чеxi 1СПО113О31Г1 переменную ill и /II ipicixii ui п 1 с им С у I ЭТО МОЖНО ДВУМЯ СН0СО01М11 Можно приев ЯП II lUlllx ЦОеМСП 1ОИ В ОТ ТС I ном операторе посте се обтявкни! \ мож ю ciciii ио з < ном опер, орс с кт Объяви|ь тип переменной и прис они си зн i luinc li i uo i ice ii hoc ic вмени переменной симво i зн iki piBuiciBt (oiicpmxp прис и mu 1 i ainni ирис з in ВДемос зна iuiiic С интлксис он p nop i и in in i изщип нок нт ни i c type var 1 10 имеет шп i т 10 0 iimcci mil т 2 Для ук иання числ i 100 ктк niiepii imuoiul о пш ио нужно j uhic lit к ik 1001 A \к вшия чце 11 100 кik впер х iiitcio ши ± сю нужно iiimcirt ык 100L 2 Эк> 1 । рующнися с 1 роковый llllcpll
70 Глава 2 Типы данных и операторы Здесь value - это значение, которое присваивается переменной во время ее создания. Значение должно быть совместимо с указанным типом. Приведем несколько примеров объявления переменных с одновременным присваи- ванием им значении' ^nt count 1J; i1 i ереме-i ой count зглас.сч «ачалэ»oe огсиение _С char cfi ' X ’ !' переменной ch i рисна! ьае.сь „имьон X float f - 1.2F; // переменной г npmааиьас_ся значение -.2 При объявлении двух и более переменных одною типа можно разделить переменные в списке запятыми и присвоить одной или нескольким из них начальное значение. Например: int а, Ь - 8, с - 19, о; // переменным b и с присвоено начальное значение В этом случае инициализированы только переменные ц и с Динамическая инициализация В предыдущих примерах переменным присваивались только конкретные числовые значения (то есть значения, коюрые нс были получены в результате вычисления какого-либо выражения) Но в Q# можно инициализировать переменные и динами- чески, используя любое действительное выражение. Например, ниже приведена короткая программа, коюрая вычисляет объем цилиндра по заданному радиусу основания и высоте: //В программе демонстрируется использование // динамической инициализации переменной. us_ng Syster; class Dynlnit public static void Кад.п() • double rad-uus - 4, height - 5; // Динамически инициализируехся double voxune - 3.1416 * rad_us Переменная volume иницнгЫизирусзся динамически во время выполнении пренраммы Console.driteLine("Обьем цилиндра равен: " - voiire); В этом программе объявляются три локальные переменные — гати s. loig'-t и vci ir;e. Первые две. racius и гещ.г, инициализированы конкретными значения- ми. Переменная voiure инициализируется динамически — си присваивается значе- ние, соответствующее объему цилиндра. Ключевых! моментах! является то, что в выражении инициализации может быть использован любой элемент, который уже был до этого обьявлен (то ес,ь действительный па время инициализации данной nepcxteimoii). Таких! элементах» может быть метод, дрхмя переменная или литерал Область видимости и время жизни переменных Все переменные, которые мы использовали до этого момента, объявлялись в начале метода Main(). Но в C# можно объявлять локальные переменные и внутри блока.
Область видимости и время жизни переменных 71 Как уже говоритесь в i лаве I блок начинается открывающемся фигурной скобкой и заканчивается закрывающей фигурном скобкой Он определяет обюсть видимости которая зависит от того, имеет ли данный бдок вложенные блоки Каждый раз создавая блок вы создаете новую область видимосли определяющую время жизни объявляемых переменных Наиболее важными об мстями витимосш в C# являюлся к колорит опредс лялолся классом и метолом На данном лапе мы рассмолрим ломко огласи» леи тиуихлп определяемую мето том Область видимости, определяемая мыолоул начинайся с олкрылзнолллси фнлернои скобки Ошако если элол мело л имеет парамелры они также вк ноч лился лз об л лсль видимости метода Общее правило состоял в том, что обьявленная внутри об лаелн видимости переменная явтяелся невидимой (ло есть недоступной) лтя кода колорылл определен за этой обтастью С те товагетьно, при объявлении переменной лз предс лад ее области видимости вы токализустс зту переменную лл защищаете се ол неразре пленного доступа и молифпкации Можно шиль чло правита касалощисся об ласт видимости обеспечивают фундамент т ля инкапсутяллилл Область лзидимости может быть вложенной Например каждый раз соз лапая блок кода, вы создаете новую итоженную об ласть лзидимости и при лом внешняя об ласть становится видимой для вложенной об ласли Эго означает члообьекты обьялз ленные во внешней области лзидимости, будут вилимы ня кола из лзнутренллеи области видимости, но обьекты объявленные во внутренней области лзидимости нс будут видимы для кода внешней области видимости Чтобы понять это прави ло, рассмотрим следующую программу // В программе демоне рируе ся зависимеело возмох!Ос i дос-лу ля к // переменном oi области видимое и, в ко орои она бьла обновлена using Sys'с- class ScopeDerto public static vClC: Main!) ( int x, // Переменлая x известна всему кеду // а пределах метода Неси') х 1С, cf (х - 10) ; //Со -,дас л я новая tie' । ви шмсс и -г:' у 2С, // Э а леремсьная буле видна с 1 < в самках / / золе блока // В данном блоке видны обе леосмелные, \ и у Cense е WriteLine ( Видны х и у > х ту) х У * 2, у тю переменная обьяи-ленн ля ио яложеннон } ____________________ ’ области втимос из полому з ксь он л нс иидн i //у ПОС, ' Ошибка1 Перемел ная у з^, •- е виД1 1 / / Вы о и яс ся дс^лу-1 < 1 сременл их । ая OuJ а с ля л // в эюи облас п в?димсс1и Console WriteLine ( Значки ch lmpi i v x равно x) } }
72 Глава 2. Типы данных и операторы Как указано в комментариях, объявленная в методе Main О переменная х доступна всему коду в пределах этого метода. Переменная у объявляется в рамках блока if. Поскольку блок определяет область видимости, переменная у является видимой только в пределах самого блока. Вот почему использование переменной у в строке у = 1С0: вне своего блока является ошибкой, о чем и говорится в комментарии. Если вы удалите начальный символ комментария, произойдет ошибка компилирования, так как переменная у невидима за пределами своего блока. В рамках блока if может использоваться переменная х, поскольку в блоке (вложенной области видимости) код имеет доступ к переменным, объявленным во внешней области видимости. Внутри блока переменная может быть объявлена в любом месте, после чего она становится действительной. Следовательно, если вы определите переменную в начале метода, она будет доступна всему коду в рамках метода. И наоборот, если вы объявите переменную в конце блока, то не сможете использовать ее до момента объявления, поскольку код не будет иметь к ней доступа. Обратите внимание на еще один важный момент: переменные создаются при их объявлении в какой-либо области видимости (при входе в данную область) и уничтожаются при выходе из этой области. Это означает, что переменная не будет сохранять свое значение при выходе из своей области видимости, то есть переменная, объявленная в пределах метода, не будет хранить свои значения между вызовами этого метола. Также переменная, объявленная в рамках блока, будет терять свое значение при выходе из блока. Следовательно, время жизни переменной ограничено ее областью видимости. Если при объявлении переменной одновременно происходит ее инициализация, то переменная будет повторно инициализироваться каждый раз при входе в блок, в котором она объявлена. В качестве примера рассмотрим следующую программу: //В программе демонстрируется использование правила, // определяющего время жизни переменной. using System; class VarlnitDemo { public static void MainO ( int x; for(x - 0; x < 3; x++) { int у — -1; // Переменная у инициализируется каждый раз II при входе в блок. Console.WriteLine("Значение переменной у равно: " + у); // Всегда выводится значение -1. у - 100; Console.WriteLine("Теперь значение переменной у равно: " + у) ; } } Ниже показан результат выполнения этой программы: Значение переменной у равно: 1 Теперь значение переменной у равно: 100 Значение переменной у равно: -1 Теперь значение переменной у равно: 100 Значение переменной у равно: 1 Теперь значение переменной у равно: 100
Область видимости и время жизни переменных 73 Как видите, переменная у инициализируется значением -1 каждый раз при входе во внутренний цикл for, затем ей присваивается значение 100, коюрос мрачивашея при завершении цикла. Заметим, что между языками C# и С есть отличие в работе с областями видимости Несмотря на то, что блоки могут быть вложенными, переменная, объявленная во внутренней области видимости, не может иметь такое же имя, как переменная, объявленная во внешней области видимости Следующая программа, в коюрои делается попытка объявить две переменные с одинаковыми именами, не 6\,iei ском п ил про ван а: /* В этой программе делается попытка обьявить во внутренней области видимости переменную с чем же именем, ч,.о и у переменной, объявленной во внешней области видимости. Эта программа не будет скомпилирована. using System; class NestVar { public static void MainO int count; Переменную count обьявлять нельзя, поскольку переменная с таким именем уже объявлена н методе м<пп() for(count - 0; count < 10; count = count+1) f Console.WriteLine("Счет проходов цикла: ” +- count) int count; // Это объявление недействительно* ! * for(count - 0; count <2; count+ +-) Console.WriteLine("В программе ест.» ошибке1”); } Если у вас есть опыт программирования на C/C++, то вам извест но. что в л их языках нет ограничений для имен переменных, объявляемых во внутренней области види- мости. То есть в C/C++ объявление еще одной переменной court в цикле ; является допустимым, но при этом данное объявление скроет внешнюю переменнхю Чтобы скрытие переменной нс приводило к ошибкам программирования, разработчи- ки C# запретили такое действие Минутный практикум " гЛ ^то опРеделяет область видимости? » ф. jl 2. Где в блоке могут объявляться переменные? 3. Когда в блоке создается переменная? Когда она уничтожается? 1- Область видимости определяет возможность доступа к переменной и время ее жизни Область видимости создается при создании блока. 2- Переменные могут быть объявлены в любом месте блока 3- Внутри блока переменная создается при объявлении эюй переменной Переменная унич- тожается при выходе из блока.
74 Глава 2 Типы данных и операторы 1ераторы [J О- предусмотрен большой выбор операюров Oncpaiopoxi является символ, .•ообщаюшии компиляторе о необходимое!и иыпотнения специфической магсмати- ICCKOH или логической операции В C# cyineci вуют че 1 ыре основных класса оиерато- )ов (арифметические, побитовые, югические и операторы сравнения}, а также несколько юнолншельных операюроилля обработки некоюрых специальных ситхации В дои lane будут рассмотрены арифметические. loiimecKiie операторы и операторы срав- тсния Кроме гою, мы расскажем об операторе присваивания Побитовые и трутне •пениальные операторы буду! рассмотрены позже рифметические операторы 1 Ся определены следующие арифметические операторы )ператор Значение Сложение Вычитание (также унарный минус) Умножение Деление э Взятие по модулю (остаток от деления) + Инкремент — Декремент 1рифмстт!ческис операторы сложения (•). вычитания (-), умножения (’) и деле- на (,) работаю! в Си так же, как в любом друюм языке программирования, они ютут применяться к любым встроенным числовым типам данных ютя действие арифметических операторов вам хорошо известно, некоторые спепи- тьныс ситуации ipeoyiOT пояснения При применении опера юра деления к цсло- истсниым данным любой остаюк будет отброшен (например, it целочисленном елении 10/3 будет равно 3) Остаюк этою деления можно полу чип» при использо- ании оператора взяптя по модулю ( ) Он работает в С-’1 так же, как в apyinx тыках, — получает остаток от не ючпслсннО!о де тения (так. К) Д- Ч будет равно I) C# опсраюр взятия по модулю может прпменя!ься как к не ючислсппым данных!, ik и к значениям с плавающей точкой (100 Гс 3 0 будс! также равно 1). is отличие т C/C++, |дс операцию взяптя по модулю разрешено производить тотько с е точисленными типахтп данных лсдующая протрамма демоншрирует применение оператора взятия по модулю В i.poi рамме демонстрируй» ч i«ci ом, jj <ао'е oi ера. ova "зял", по уст.учк. -its УосЮете tL.r-._-..: static vo»l Уа-г, () int ..m.ll. »rcr; douo.e are-; i ri'.'.i..:. . 0 / 3; item - 10 < 3;
Операторы 75 - ''Г.sole . Wri t eL_n<? ( " Рсзуль Ja г цслоч/олсию-о „еченил 10/3 равен: -^result т " 4 пос* а _ ок paaei : " + _гс” ) ; Сс г ьс_е .’лг xteL_ne ("Резульга ? деления чисел, имеющих .иь aojoie, — ^О.С'З.З разег : \r.” т arcsu1с - "\п остаток равен: " * агег) ; Цро1рамма выводит следующий резулыл. целочислип.ою деления 12 3 равеь : з а. ок равен: 1 >_yioj.ai деления чисел, имеющих ми. aouole, lG.C-'э.С ра^еа: о.с?ЗЗзаЗЗЗЗЗЗЗЗ ос.а-хок равен: 1 Как шиите, оператор взятия по модулю возвращает значение I как при работе с целочисленными операндами, так и при работе с данными, имеющими тин т Дце. Операторы инкремента и декремента Об операторах инкремента (++) и декремента (--) мы уже упоминали в тлаве I. У эт их операторов есть некоторые специальные свойства, которые делают их очень полез- ными при задании алгоритма изменения переменно!! цикла (например, в цикле ter). Рассмотрим, как функционируют операторы инкремента и декремента. Оператор инкремента добавляет единицу к своему операнду, а оператор декремента вычитает единицу. Следовательно, оператор выполняет ту же самую последовательность действий, что и оператор а оператор выполняет ту же самую последовательность действии, чго и оператор Ооа ли оператора могут указываться либо до операнда (как префикс), либо после (как постфикс). Например, оператор можно записать гак: '< о. ерагор указываете* до oi «.ранда " пт так: ' ' оператор указываемся после о: сракда В лом примере оператор инкремента указан двумя способами, между которыми нет никакой смысловой разницы. Но когда оператор инкремента или декремента исполн- яется как часть большего выражения, то от расположения оператора по отношению к операнду зависит алгоритм выполнения блока кола, бели оператор указывается "ерег) операндом. С - увеличивает значение переменной (операнда) до того, как зга беременная будет использована оставшейся частью выражения. Пли оператор ука- зывается после операнда, го Си увеличивает значение переменной noc.ie того, как
76 Глава 2 Типы данных и операторы оставшаяся iac l выр i Кения hciio п к, > у переменную в своих опер иш i\ I’iccmot- рИМ С IC IVIOIJUIII пример В ЛОМ С 1\Ч тс переменной I 1(> и н ко I бе 1с г и тис in ое те! присвоено ?н тчсиис II IK io виа I I ic Переменной >y те i щ испоено in i Юнис К) пнем iiiiiciuii i с реме иной бутст vhc hi iciio hi е шпине В оно 1У cieiuie. niiiciiiic переменной будет VIICIIIILHO loll pu IM 1НС COCIOHI IO IbKO НО Времени KOI II HO про I «III IC I biaroupo najBiiiniiM cbohcibim оиернорн инкремента и 1скрсмсп1 i пп роко ис- ПОЛЬЗУКЛСЯ при формиров ШИП IIIOpniMOB Операторы сравнения и логические операторы В 1срмипа\ оператор сравнения и ю ичш ии оператор с юно сравнение озн Biaci оиенк одною значения по ерщнению с ipyinei । шово Юенческии способ который можно свяять значения и (пенни и тожь) в выражении Поеколыс рез\1ьтиом выполнения опертгоров сравнения являются буквы зн 1чсния, эт операторы часто работают совмести с то и icckiimii оператор 1ми Полому их можн рассматривать вместе Ниже пре тставкпы one pi торн ср nine пня Оператор Значение Равно Не равно > Больше чем < Меньше чем >- Больше или равно <= Меньше или равно Далее перечне юны огичсскпс опер i оры Оператор Значение & AND (И) I OR (ИЛИ) л XOR (исключающее ИЛИ) && Short circuit AND (быстрый оператор И) 11 Short circuit OR (быстрый оператор ИЛИ) 1 NOT (НЕ) РеЗУТЬТагОУ! ВЫПОИ1СПИЯ опер поров ертвнепия и тогических операторов ЯВТЯЮТС значения lima В СТ опсраюры и мо ут применятся ко вссу! обт.сктам ьтя их сравнения Г iipciMci равсишв! и in HcpmcnciBa One риторы ср ншення применим юнко к перечисляемым иным тайных которые споря точены в своей сгрхкту! (например упорядоченная с руюурт incci I 3 u i тк 11 ice и nt споря tone НН
77 Операторы имволы букв алфавита). Следовательно, все операторы сравнения могут примснять- ко всем числовым типам данных. Однако значения типа cocl могут сравниваться тотько на предмет равенства или неравенства, поскольку значения true и fslse нс порядочены. Например, выражение true > false в языке C# нс имеет смысла. Дтя логических операторов операнды должны иметь тип bool. Результатом логиче- ских операций также являются значения, имеющие тип bool. Логические операторы ь ।, - и ! поддерживают базовые логические операции AND, OR, XOR и NOT соответственно, результаты выполнения которых соответствуют значениям, приве- денным в следующей таблице истинности. р q p&q P q рл q false false false false false true true false false true true f alse false true false true true true true true true true false false Например, из таблицы видно, что результатом выполнения оператора XOR будет значение стае, если только один его операнд имеет значение true. Рассмотрим программу, в которой демонстрируется несколько операторов сравнения и логических операторов: // В программе демонстрируется использование логических операторов // и операторов сравнения. using System; class RelLogOps { public static void MainO { int i, j; cool bl, b2; г - 1C; j - 11; if(i < j) Console.WriteLine("i < ]"); if(i <~ j) Console.WriteLine("i <= ]"); !- j) Console.WriteLine("i I- j"); — J) Console. WriteLine ("Эта строка не будет выведена."); lf(l j) Console.WriteLine("Эта строка не будет выведена."); -i(i > j) Console.WriteLine("Эта строка не будет выведена."); Ь1 - true; Ь2 - false; & d2) Console.WriteLine("Эта строка не будет выведена."); - *(!(Ы & Ь2)) Console.WriteLine("Результатом вычисления"+ " выражения !(Ы & Ь2) будет значение true."); - * (°- । Ь2) Console.WriteLine ("Результатом вычисления"-г выражения Ы I Ь2 будет значение true."); - -(bl л Ь2) Console.WriteLine("Результатом вычисления"* ” выражения Ы л Ь2 будет значение true."); )
78 Глава 2. Типы данных и операторы Результат выполнения программы следующий: 1 < J 1 < - ] т 1 - 3 Резуль.аюм вьни^лсия выражен иъ ю* л< сО Суде 31 аче) щче Резульа_см вычисчеьуя ьырахсч гя £ 2 О . ли - at ачи'/е ~л к . Резулыа.ом вычисления выражения гы Ь2 Руде- знзче nt' rue. Быстрые логические операторы В С? предусмотрены специальные быстрые версии логических операторов А'.ь и при использовании которых программа будет выполняться быстрее Рассмотрим, как они работают. Если при выполнении оператора and первый операнд имеет значение fa 1 se, результатом будет fa 1 se, независимо or значения второго операнда Если при выполнении оператора GF. первый операнд имеет значение true, результатом будет значение true, независимо от значения второго операнда Следовательно, в згих случаях нет необходимости оценивать второй операнд, и соответственно программа будет выполняться бысгрсс Быстрый оператор AND обозначается символом «i, а быстрый оператор PR - симво- лом | Единственная разница между обычной и быстрой версиями операторов состоит втом, ч ю обычные операторы всегда оценивают и первый, и второй операнды, а быстрые операторы оценивают второй операнд только при необходимое!и. Ниже приведена программа, в которой демоне г рируется использование быстрого оператора ANN Программа определяет, является ли значение переменной о кратным значению переменнои г Это осуществляется с помощью оператора взятия по модулю Если остаток деления гъ d равен нолю, то значение переменной т является делителем значения переменной п. Однако поскольку операция взятия по модулю предусматривает выполнение деления, в программе используется быстрый оператор А1т„ титя предотвращения деления па ноль. // В программе демонеарируе.ся ист ользоватглс оис.рсю оператора AND. us_rig System с. ass SCops t public static void Main() < .nt n, a ; n - 1C; d 2; -f(d ! 0 &S (n d) — u) Console .'.'.'ru et ic (d г " является делителем числа " п) ; d 0; // Теперь грисваиваем переменной d значение 0. // Поскольку перемет.пая а инее, зтачение С, второй операнд 11 нс оцениваемся. Применение быстрого оператора j.f(d 0 && (г. о) С)-<—------------пре гогвращаст ае тение на но ть Console.Wr.teLine(а - " является делителем числа " п); /* Тег.ерь в выражении условия оператора -f Ьудег использован обычный оператор AND, ч.*о приведет к делению на ноль. */
Операторы 79 { (с: । 0 & (n od) 0)4---[Оцениваются оба операнда по привод»! к де тению на ночь | Console Wr 'с1.гд(‘Т г ' являыся дели едем числа " -г п) , } 1 .1Я предотвращения деления на ноль вн лча к оператор проверяет по равно ш но по значение переменной be ш равно го быырыи оператор е ос, шавлив ,сл 11 шнсишес вы по шенис онера юра и де теине по мо И по по bi ii io шяе 1ея 11оско ,ьку в 1ро,раммс в первом случае значение переменной равно 2 выполняется операция нт ния по моту по Во втором с течас переменной присваивается заве юмо непри- емлемое значение 0 и операция взят ия по мо ту лю нс вы по тястся в резу п>1 не чею пре юл вращается те темпе па но ть Наконец в третьем с iviac цело [ьзуется обычный оператор and При пом оцениваются оба операнда чю приводи, к ошибке выпо пения программы при попытке деления на ноль Проект 2-2. Вывод таблицы истинности логических операторов pl LogicOpTable cs В этом проекте рассматривается создание программы которая выводит па экран таблицу истинности логических операторов В программе были использованы э тс- менты C# — одна из eseape-последоватс ,ьностеи и ло, ические операторы, описанные в этой главе В проекте также демоне фирустся раз тичис в приоритетности между арифметическим оператором + и логическими операторами Пошаговая инструкция I Создайте новый файл и назовите его - j "т 2 Для выравнивания столбцов таблицы используитс escape-noc ic юватс юность которая вставляет симво ты табуляции в каждую троку вывода Например прсд- ыав тенныи ниже оператор т те() выводи! зато ловок пя таблицы Console WriteLine(”P\tQ\tALD\tOR\tXOK\tNOl'), 3 Для каждой следующей строки таблицы используитс символы габ\ тяции, члобы поместить результат выполнения каждой операции под соответствующим заюповком 4 Ниже пре тставлен по плыи код программы который нужно ввести в файл Loyx- alOpTable cs /* Проекх 2-2 Выводина печать таблицу ис ин1 ос и */ using System, class LogicalOp^able { public static vo_a Ma^n() t bool p, q оп/чес<их о ep. a odd s
80 Глава 2 Типы данных и операторы ronso е I. L г с Ь \ QXLAM f-1 & ) (F л j q •) С КО мп И I Иру ГИС ГСП 1 С IL LVKHIUIH 1 Г 1П\С 1И с upoi р 1мм\ В резу U 1с сс випо тения 6} 1С ! BLIBC- 1 io нт 1 е АГ и ‘>В \С и NG Хе X г о L U 11^ <с ОЧ С 1 1 < tie 4 с 1 t t х е 6 Обр пин внимание ЧТО Ю1 и icck ic oi i paiopi I VK hi ih icmihc IT Ml IO I l\ и о в каюснтс ii.'pivtiipoB Hkiw'iuiM н кру| пас скобки Эю нсоо- хосимо поско и>к\ в С ч опер пор нмес бонс высокии приоритет чем юти ic- ck ic oi ера юры 7 [|опы1аи1ссь с 1МОСЮЯИ т но моанфииировиь npoiртммх таким обратом чтобы на экран BbiBouiiitcb ci мнонт I и () вместо с юн v и Возможно это з । тач i окажется иэстато ню с южной1 Ответы профессаонала Вопрос $11смв( nv-Kill I ()61 I П!1 к оперпоры II Cl III в нско opi IX с I' ч |яч быстрые опер 1'opi । яв 1 потея бо не эффект иными ’ Ответ Ино! и в опер пор i\ и ipedveiCzi онснип обтонертпта 3 о и итерируется с icivioincii протртммои Ей* о ж pci рамма К J Й Д М. р/ру<_ Ч рЗЗВИ ( ч f- ч .1 Пр в
Оператор присваивания 81 х В ахе1 2 3- ил \ оля о ера ot з £ < i ц х_м t t о pzMct Gt-а о au я ^кремн i, -.зжг* t v с я жь м c fd ье «x 1 < g ) ) Console far ex ( Э а слро<а не луде ^.c /'• Следующим oi ера юром буде аыаеденс / зн 1че1 ие i сселенной 1, равн е nr еле лг-teL^rf ('Ecin oi ера р лнкреме; с 6yj i с к, ’ 1 ерсмсн! ая _ i риме~ з a^tue И, ’ р э.ом случае < i ерсмсч ьои te by е р им i r iji/я инкремента, icuxoj оку pgicj -*ye ч bwciri -* j П 4_ (la^be && ( - _ < _ л ) ) Сопьохе WriteUnc ("Э.а с po-a n< бул bl / Следующим oi сраюром вн obl буле мне ( // значение i временной i, равное 1 console fariteL^ne("Если oi ера op гикрем^нп Руд t н i ” переменная i iримех тюние ’ • ) комментарии указывают, что в выражении vc ювия перво о онер нор' ю к пере- менной 1 будет применена операция инкремента независимо ог uuhhikx ш выра- жения Но при использовании быстрою оператора значение переменной те будет \в тичсно на единицу, поскотьку первый оперта имеет значение 1о есть з oi пример показывает, что ест в коде необходимо вы юнннь нртчко' \ енсран та, н годящегося справа от оператора А\1Г\ вы дотжчы щпо ” юва i обы hiv»o форму > ого оператора (то же касается оператора од) Минутный практикум I Какие дсиония проияюдиг операюр тян данных он voxel быть применен’ 2 Какие типы данных moij । исно п> юна и и операторов’ 3 Всегда ли быстрый операюр оценчви об i Оператор присваивания Мы уже пеоднокрашо исноibjoim in операюр присваивания в примерах программ нои кни1и, а теперь рассмотрим сю 6oiec подробно Оператор присваивания указы- ваеюя как одинарный знак равенства (-) В C# он работает практически гак же, как I Оператор взятия по модулю возвращает остаток целочистенною деления Он может быть применен ко всем числовым типам данных 2 Операнды логических операторов должны иметь тип no 1 3 Her, быстрый оператор оценивает второй операнд тишь 101 i.i, ко) а р, )\ и > н ныр1жич1я не )ьзя опредетить, исходя m истинности ю и ко первою оперт ю
82 Глава 2 Типы данных и операторы в любом другом языке программирования. Оператор присваивания имеет следующий синтаксис: vaz - expression; Тип переменной должен быть совмссшм с пятом выражения Оператор присваивания имеет одно очень интересное свойство, с которым вы, возможно, еще нс знакомы, — он позволяет создавать «цепочку присваивании». В качестве примера рассмотрим фрагмент кода: int х, у, z; х у - - 100; // Переменным х, у, z присваивается значение ICC. В приведенном выше фрагменте с помощью одного оператора переменным х, у и z присваивается значение 100. Такая последовательность переменных и операторов допускается, поскольку оператор = присваивает переменной, находящейся слева от нею, значение выражения, находящегося справа. Следовательно, выражение z = 100 будет имс!Ь значение 100, которое присваивается переменной у, а затем — перемен- ной х. Используя «цепочку присваивании», можно одним значением легко инициа- лизировать группу переменных. Составные операторы присваивания В С#, как и в С/С+-1-, имеются составные операторы присваивания, в которых арифметические операторы совмещены с операторами присваивания. Рассмотрим применение составного оператора присваивания на примере. Выражение х - х + 10; может быть записано с использованием составного оператора присваивания X +~ 10; Пара операторов += указывает, что компилятор должен присвойгь переменной Х' значение выражения х + 10. Приведем еще один пример. Выражение х - х - 100; можно записать как х = 100; Оба оператора присваивают переменной х значение х IC'j Для всех логических операторов (то есть операторов, требующих два операнда) сущест- вуют составные операторы присваивания. Синтаксис этих операторов следующий: var op = expression; Таким образом, в СТ имеются следующие составные операторы присваивания: +- -=. /_ %- &- |- Составные операторы присваивания в сравнении с их «обычными» аналогами имеют два преимущества. Во-первых, они более компактны, а во-вторых, их использование позво- ляет ускорить компилирование кода (так как операнд оценивается только один раз). По этим причинам составные операторы присваивания часто используются в про- фессионально написанных CS-программах.
Оператор присваивания 83 Преобразование типа в операциях присваивания g программировании часто применяется присваивание значения переменной одного тип i переменной три ого in ia Например вы можсте присвоить шаченпе перемен- ной ины переменной тина ti г, как показано ниже рг в(,е!ие качения «.ила int переменной ли a f_uoat Ecin в операции присваивании используются совместимые тины личных. то чип переменной нахо ннцеися справа от оператора автоматически преобразуется в гни переменной нахо тяшеися с тева от него С тедоватетьно в пре тыдхгцсм примере inn переменной прсобра!\ется в тип f . <'j , азспем значение переменной i присваивается переменной Но поско тьку C# является языком со стротим контротем типов го нс все ею типы тайных яв тяготея совместимыми, и стедовате тьно, не все типы преобразо- вании разрешены Например типы данных cool и int несовместимы При присваивании отного типа тайных переменной другого типа автоматическое преобразование типа происходит, если О два типа данных совместимы, О конечный гни (стена) больше исходного типа (справа) При выпо шении двхх лих условии происходи г расширяющее преобразование Напри- мер котнчества битов, вы те тсиных для типа данных int всстда достаточно для хранения всех теистиите тытых значении типа ' Д', а иоскотькх оба ни типа неточно тонные то к ним может быть применено автоматическое преобразование Дтя выиотнения расширяющих преобразовании все чис товые типы, включая цето- числснпыс данные и данные с плавающей точкой, являются совместимыми Напри- мер, вс тетхющеи программе выполняемое преобразование действительно, поскольку преобразование чанных типа Long в double является расширяющим и выполняется автоматически // Ь демов^грируесся авхома. ичесгое iреобразование ина // 11 1ы_нин из .ong в ^.oub^e tsд-гд (д, С dSs ч at с vo d Mnn() { q l, 1 v D, Автома1ичсскос прсобраювание чипа значения из lor g в icuble е Wr tclireCL i D ” 4 L ’’ " + D) , Хотя °брал c\шествует автоматическое преобразование типа переменной из long в doabie, нос автоматическое преобразование осуществить нельзя, поскольку оно не будет
84 Глава 2 Типы данных и оператор расширяющим Следовательно, такая версия предыдущей программы будет недещ ни I е плюй Г с рамма ск м у' 1Д "1 в ci 1- а Г 7 эуЬ^С-1, ass L С о О puD^.c Stacie vo_a Г'ахгн) ong L -OdbxC J Нс н>зя aBTOMdi ичеджи преобразовав переменную типа couble в inn long D 00.23283 0, L D, / iam преобразование недеисх аи.ельно Conso-ив Wr.teL.ne (" L и D ' L " ' +- D) , В ioiiojhchhc к уже указанным ограничениям необходимо добавить, что не сущее Ву'ст автоматического преобразования между типами decimal и float иди doub] гибо преобразования переменных чистового т ипа в переменные типа ct аг иди boo Переменные 1 инов "jrni д такие несовместимы друг с другом Выполнение операции приведения типа между несовместимыми типами данных Автоматические преобразования очень удобны, но они нс отвечают всем потреби стям программирования, поскольку в них допускается расширяющее преобразован] только между совместимыми типами данных Во всех других случаях приходит применять операцию приведения тина Приведение типа — это инструкция комп: гягорулля преобразования одного типа ланитах в другой Такое прсобразованиетипг тайных является явным Операция при тедения типа имеет с тсдующии синтаксис: 'I itget-type) expansion Конечный тип (target-type) указывает, к какому типу должно быть приведено выражен! 1ак, сети необходимо прсобратопать выражение х/у к типу .nt, следует написать’ аоио.е х, у, // (хПС) (х/у) В ном примере приведение типа преобразует результат вычисления выражения типу nt, хотя переменные х и у имеют тип oonble Круг тые скобки, в которЫ зактючено выражение х/у, явтяются обязательными В противном стучас приведение к типу - * будет применено только для переменной х, а нс для значения результат астения Здесь применяется операция приведения типа, поскольку автоматичесКО преобразование данных типа io role в тин inr не может быть выполнено Когда в результате приведения типа происходит сужающее преобразование, ипфорМЯ ция может быть утеряна Например, се ги при приведении данных типа long в 1П’ значение переменной, имеющей тип long, выйдет за диапазон значении, допустим! для типа int, то биты старшего разряда будут удалены, счсдовательно, информат
рЬ1ПоЛнение операции приведения типа между несовместимыми типами данных 85 , меряна Ее in значение c iihbuouiiii ючкои привошюя к щ кнпс luinnen ж- _ ........z./ i it l/t lltTrl I И 1\>1 II1M Г I t( >11 I И 11 I I <31 II Г типе 1, отор в-1 резе п Hii/гл ipc i1-1 1151СН 1 npoipiMM I с НС 110 I фобиые компоненты уфачиваююя носко п>к> при i ikom прсоор i «>в ihiiii они 1В1Ю1СЯ Например се in JH1 ICHIIC I 24 нреобр в\с ю i в нс точне ichhi in inn преющим 6\ lei in incline I а фобная iich 0 23 6\ ют енрипа icMOHcrpiipeKMii 1Я iiuoiopiie вн и i npcoopuo пип iOBinncM оперший прпве ninm in la м При М )М I ptOOpilOI 1НМИ lp( t Н ЯС) сив 1ЯЮ1111Я 1ИС [ 1 nv lc 1 О Ор( ПК и Вьражснге vn к jet. Здссл UHHIIC НС терякпея ПОСКО I К\ 1ННСНИ1 |П0н1Х)1И Я I 111 4 нс Дс IIVCIИМЫХ JHd КНИИ Ollpc К СИНИХ 1Я HI ILy В ЭТОМ С (V I ie информ щия it ряс И Я I ОС КО II К} ЗН i ichhc И 1X0 11 |сЯ 3 1 I [LpClCldMM JHiUHOHd допуешмых 1Н1ИНИИ OI pc ic ICHHIIX 1ЛЯ llllll I r L nc (’ .значение i с-ремеь д L p 11 ) 31^.3 hi/e \SC^ хода д я сима ла X а ) al) D ----------|выпо 1ЕСНИС операции ИрИВС! НИЯ 1И I Mi Ж I.V HLCOHMCCII МИМИ 1И1ПММ Резу iiiii BI ню ження программы выг гя гит с it 1\ ОЩНМ ( 5] ЮМ Целая СЬ X ь|зженил х у рзднз 3 с etc.»-нои Ь сачно ОС itL.Hb о/ о раы о 1 JTOH ло))( р IUML Hplllic ICHIIC рсзе Tl Г IT l ВЫ I ИС 1СН11Я Blip IAC 1ИЯ К I II IV Pl,li°ini к о opiiwHiHiiKi ipo6non части и потере пнфо| м шин 3ncvt ю ц Н° 1 присваивюса значение 101) ипформаци 1 нс тсрясюя поскоцт )о Во ,C""L 1,1X0 пися в щапазоне допустим! lx зн Щепин очре tc ипных ия i inii мац"^" l1,lblIkc присвоить переменной ЗН1ЧСНИС 2з7 происхо шт поюря пнфор Лещ " 11<>,1,х,х ,го nic ю 2x7 пре выш ic г максима п но юпсстимое знисннс опрс к T1ina°L Н 11111,11 ’ И наконец при присваив тин зпа ichibi inn i переменной Информация НС тсрясюя НОСКО IbKV ИСНО Ц IVC СЯ прпве 1СНИС 1ИП I
86 Глава 2. Типы данных и операторы Минутный практикум j “% I. Что такое операция приведения типа? qX 2. Можно ли без выполнения операции приведения типа присвоить значение типа short переменной типа int? А значение типа byte — переменной типа char? 3. Запишите оператор х = х + 23; другим способом. Приоритетность операторов В табл. 2.3 показан порядок приоритетности всех операторов C# от наивысшего к самому низкому. В таблицу включено несколько операторов, о которых будет рассказано позже. Таблица 2.3. Приоритетность операторов в C# Наивысший () [] ++(постфикс) -(постфикс) checked new sizeof typeof unchecked ! ~ (cast) +(унарный) -(унарный) ++(префикс) -(префикс) * / % + < > <= >= is == != & Л I && II = ор= Самый низкий Выражения Операторы, переменные и литералы являются компонентами выражений. Выражени- ем в C# является действительная комбинация этих компонентов. Если вы уже занимались программированием (или хотя бы изучали алгебру), то, вероятно, имеете представление о синтаксисе выражений. Однако при работе с ними нужно учитывать несколько важных моментов, о которых мы сейчас поговорим. Преобразование типов в выражениях В пределах выражения существует возможность совмещения двух и более различных типов данных при условии, что они являются совместимыми. Например, вы можете I. Операция приведение типа — это явное преобразование типа данных. 2. Да. Нет. 3. Оператор можно записать как х +- 23,-.
Выражения 87 u>B\n.iuJibiJ выражении ыниыс гипа s>- г н] поскольку оба зги типа яв гяются ч1к юными he in в выражении совмещаются различные пшы тайных они преобра- зимся в о сны и тог же гип на основе а иоритма пошаговою преобразования (го есть в looibcictbuh с прпоригегноегью выпо тения операции) Преобразования выпо тяготея носредшвом испо гьзования принятых в С~ npaeui автоматического преобразования типов в выражениях Ниже представ юн алгоритм, опре ю генный згими прави гами для арифметических операции j [ с in олин операнд имеет тип го второй операнд авломаг ичсски преобразхшея к шву Те да1 (за исключением случаев когда он имеет гип или а в злом с гучас произойдет ошибка) Э ) с пл о 1ИН из операндов имеет ши i m, второй операнд автоматически нреобразуегся к типу rble 3 I с hi один операнд имеет тип fbc1- вюрои операнд авюматичсскп прсобразх- стся к тину *- О he hi отин операнд имеет тип ulc-д второй операнд автоматически преобразу- ется к типу uloij (за исключением с 1учасв, кота он имеет гип -myt^ bor" rr или в этих случаях произоиде! ошибка) 3 t с ги один операнд имеет гип 1сга второй операнд автоматически преобразуется к типе д 3 I сти один операнд имеет тип г" а второй операнд — тип sb_,t°, nbo> или ", то оба операнда автоматически преобразуются к типу icrg 3 I с ти один операнд имеет гип uine, второй операнд автоматически преобразуется К ТИПУ 1.1 г 1 3 Если ни одно из вышеуказанных правил не применялось, оба операнда преобра- зуются к липу II t Обратите внимание на некоторые моменты, касающиеся правил автоматического преобразования типов Не все ,ипы данных могут совмещайся в выражениях ( 1 частности невозможно автоматическое преобразование данных типа tx^at и ти влип de - ridi и невозможно совмещение данных липа л n jc тюбым трхгим 1ИП0М знаковых це точис генных данных) Совмещение этих типов требует использо- >1пия операции явного приведения тина Впиматс 1ьно прочигаигс последнее прави го, в котором говорится что если ни одно из вышсперсчис генных правил не применялось, то все операнды преобразуются к гппх Следовательно, все значения, имеющие тип с.-и", зЪх-<=, Ь < . тоге и ri, в выражении преобразуются к типу int для выполнения вычислении Такая гронсдура называется автоматическим преобразованием к цезочис ценному типу Эго кже означает что результат всех математических операции бхдет иметь лип, мпорому т гя хранения значения виде юно не меньше бигов чем типе В 1ЖНО понпмагь, что автоматическое преобразование типов применяется к значсни- 1м (операндам) то п>ко ioi та koi ia выражение вычис гястся Например сс ги внутри поражения значение переменной типа byte преобразуется к типу nt то за прсде- 'амп выражения переменная сохраняет тин T yte Учтите, что авюмагическос преобразование типа может привести к неожиданному результату Например, когда арифметическая операция проводится с двумя значе- ниями типа Lyto, выполняется такая последоваiсльносгь действий внача ю операц- ия b' te преобразуются к типу ы ‘ , а затем вычисляется выражение, результат которого тоже будет иметь тип mt Рсзх гыаг операции над двумя значениями о го
88 Глава 2 Типы данных и операторы будет имегыип j nt Эю довольно неожиданное следствие выполнения вышеуказанного правила, поэтому npoiраммисг должен кон гредировать тип переменной, которой будем присвоен резулыат Применение правила автоматического преобразования к целочисленному типу рассмафивается в следующей программе: / В программе димонг рируегся применение правила автоматического / преобразования делочис 1енг с-му ihv. pur _1С static V- а Га^п() < b - (byte) (Ь ж с); !' Тин результата должен бытэ приведен :/ тику переменной б. Приведения шпон нс требуется, когда результат вычисления выражения b ’ b присваивается переменной ±, так как переменная о автоматически преобразуется к типу 1 -л при вычислении выражения Но сети вы попытаетесь присвоить результат вычисления выражения г * t> переменной о, необходимо будет выполнить операцию приведения типа обратно в ши Dy: о1 Вспомните это, если получше неожиданное сообщение об ошибке, возникшей из-за несовместимости типов в выражении. Такая же ситуация возникает при проведении операции с данными типа char. Например, в следующем фра: менте кода необходимо выполнить приведение типа результата вычисления выражения обрашо к шпу с"аг. поскольку в выражении переменные -nl и '1 2 преобразуются в тип хг : : char chi - 'a', ch2 - 'b'; -hl (cnar) 'chi - ch2) ; Без приведения тина результат сложения значении переменных сг,1 и сп2 имеет тип тп‘ и не может быть присвоен переменной типа char Приведение типов примснясюя нс только для преобразования типов при присваи- вании Например, в следующей пршрамме для получения дробной части результата вычисления выражения результат приводится к типу сс ible, поскольку без операции приведения он имел бы тип i-.u и его дробная часть была бы утеряна </ В । poi рамме демонсг1 puj < ехсч i римеь 01 ераиии ^ризеиенкя ы.а •' ' к резуль.зпу вычисления вырахеьич. чъхпд .ass UseCast
Выражения 89 DUolic static void Main() { lord - 0; i < 5; Console . WriteLine ("Целочисленный резульюч нычксленлл выражения " -i+"'3: " i ' 3); Console.WriLeL-ne("Резульгывычисления выражения, аызодймый” т "с дробной честью: гО: #.##}", (clcuo^e) i • 3); Console.WriceLine(); Ниже показам результат выполнения этой программы: цеа.о численный результат вычисления выражения 0/3; С Резул^га!' вычисления выражения, выводимый с дробной частью: целочисленный результат вычисления выражения 1/3: О Резчлотат вычисления выражения, выводихчый с дробной час;зю: .33 Целочисленный результат вычиаения выражения 2/3: О Результат вычисления выражения, выводимый с дробной частью; .67 Це ючисленный результат вычисления выражения 3/3; 1 Результат вычисления выражения, выводимый с дробной часдою: 1 целочисленный результат вычисления выражения 4/3: 1 Результат вычисления выражения, выводимый с дробной часыю: 1.33 Ответы профессионала Вопрос. Происходит ли преобразование типов в выражениях с унарными операторами, такими как унарный минус? Ответ. Да. Для унарных операции операнды, у которых диапазон допустимых значении меньшим, чем у nt (такие, как t yte, soyt'i, snut и ), преобра- зился к типу int. Операнд типа cbn ткже преобразуется к типу п- Кроме того, cum операнду типа tint присваиваемся огрпцаюлыюе значение, он преобразуется К типу long. Использование пробелов и круглых скобок • i тя улучшения читабельности программы выражения в €’•'• могут содержать символы '••оуляции и пробелы. Например, следующие два выражения аналогичны, но второе •мраздо легче для чтения: 3-у*(127/к); 10 / у » (127/х); круглые скобки повышают приоритет операторов, содержащихся вну три них (скобки '^пользуются точно так же, как в алзебрс). Использование дополнительных крутых Чибок не приведет к ошибкам или замедлению вычисления выражения, полому их Ч|ожно применять для определения точного порядка дейечвии и, следоващльно, для
90 Глава 2 Типы данных и операторы j гучшсния читабельности программы Например очевидно чю второе выражение читастся проще чем первое х у: < гр-. 2 / х (у 3) (34*1._1тр) 12 Проект 2-3. Вычисление суммы регулярных выплат по кредиту ;У| RegPay cs Как уже говори юсь тип данных гтэд особенно удобно исно гьзов гть и денежных вычис гениях Предлагаемая программа вычисляет с>мм\ регулярных ныгг гат по кредиту (например на покупку автохлобилг) Принимая натальные ггиныс срок крсгитг гис го п га те же и за юг и величину процентов по кредиту лрограугма выгистгег размер о гною платежа Поскольку ато фин тисовые вычисЛенин для пре зетавления данных имеет смыс г использовать тип ч- 1ГГа в эгом проекте тсмонстрируется применение операции приведения типов а также мелодл3 д () из библиотеки C# Для вычисления размера платежа используется с гсдуюшая форму га _ IntRate* (Principal / PayPciYear) Payment =-------------------------—и—у— I ((IntRate / Pay PcrYcar) + 1) ,a>PerYear NumYtari где в переменной ^-i 5 указывается процент выпит по крсилгу в переменной Рг п.- ра± содержится значение стартовою ба ганса в переменной "а . - аг указывается чис го платежей в год а в переменной Hcrteais задается срок погашения кредита в годах Отметим что в знаменателе форму ты вы должны возвести в степень соотвстсг веющее значение Д ля этого используется малематическии метол м_, г f (; Вог как он вызывается rebUj-t Path Pow(nase exp) Метод atn () возвращает значение основания степени ( < ) возвг кнното в по- казатель степени (._ xi ) Аргументы метода ° л () догжиы имегг тин d te и возвращаемое мсготоул значение также бу ict иметь тин . Элоозшчаст что вам необходимо использовать приведение типов для прсобртзования тип г r-j 1е в тип се Пошаговая инструкция 1 Создайте новый фаги и назовите его 2 Иенозьз} н го в нрснрзммс cTtuviouiHL переменные de de. аес-пад асс±1т а [I ПС г х n Р а с °ау Pt. г Ye а г vearb Pay ent dt ие ci Проие аы а / КС РДс,С £50 1 а / Срок I счаше! ия // Pci амер iaie>ea
Выражения 91 decimal nuirer, denom; // Вспомогательные переменные. double b, e; // Основание и показатель степени // для вызова метода Pow(). (Поскольку большая часть вычислений будет осуществляться с использованием данных типа decimal, большинство переменных имеет тип decimal). Обратите внимание, что за каждым объявлением переменной следует коммента- рий. объясняющий! ее использование. Это помогает понять, какие функции выполняет каждая переменная. Хотя мы нс включали такие комментарии в большинство коротких программ, приводимых в этой книге, нужно отмстить, что с увеличением объема и сложности программы эти комментарии часто становятся необходимыми для понимания алгоритма. 3. Добавьте в программу строки кода, специфицирующие информацию о кредите. В программе заданы следующие данные: значение стартового баланса равно S10000, проценты по кредиту составляют 7.5%, число выплат в год — 12, а срок погашения кредита 5 лет. Pr_ncipal - 10000.00m; IntRate =- 0.075m; PayPerYear - 12.0m; LumYears -- 5.0m; 4. Добавьте строки кода, в которых производятся финансовые вычисления: turner = IntRate * Principal / PayPerYear; e - (double) -(PayPerYear * NumYears); r (double) (IntRate / PayPerYear) + 1; cenom -= 1 - (decimal) Math. Pow (b, e) ; Payment - numer / denom; (Обратите внимание, как нужно использовать приведение типов данных для передачи значений методу Pow () и преобразования возвращаемого значения. Помните, что в C# не существует автоматического преобразования между типами данных decimal и double.) 1 Завершите программу оператором, который выводит значение ежемесячной вы- платы: Console.WriteLine("Размер ежемесячной выплаты: {0:С}", Payment); <' Ниже приведен полный текст программы RegPay.cs: Проект 2-3 npoi рамма вычисляет размер ежемесячной выплаты по кредиту. Назовите этот файл RegPay.cs. using System;
92 Глава 2 Типы данных и операторы В рсзу и 1,1и ишю тения преяр 1\1мы б\ ц । выведена с тс вюпня строка 1 Iporectпруиге лу пр01рГУ1му прежде чем иерсиги к изучению шыппшио \i нерпа ia (ввеипе раличпые значения суммы кредита, сроков погашения и процентов ио кредту)
Контрольные вопросы 93 вопросы 1. Почему в С- строю специфицирх тотся диапазоны топхегимых значении и характеристики его простых типов' 2. Чю представляет собой симво тьнып тип в Ся и чем он оз тичасзся от символьною тина испотьзхсмою в дрхтих языках протраммиротзанпя’ 3. Справедливо ли утверждение что переменная типа> щ может хранить любое значение, поскольку любое ненулевое значение явтястея истин- ных! ' 4. Испо тьзуя одну строку кода и escape-нос тсдоватс тытосги, напишите оператор г. 1 <[ _l re () ,, в резх плате вынолненпя которого бу туг вы- ветены следующие три строки Первая Вторая Тре^оЯ 5. Какая ошибка содержится в данном фрагменте кода’ fo г(_ - О, с < _0 , ст г ) inc sarr. sum — sure -с Console Wr , L< l.jr < ("Сумма иавна ' + sue), 6. Объясните различие между постфиксной (нт) и префиксной (+^б) формами оператора инкремента 7. Напишите фратмент кода, в котором т тя предотвращения ошибки деления на ноль использован быстрый оператор -н 8. К какому липу преобразуются типы г /г^ и ъ лт' в выражении' 9. Какой из нижеприведенных липов не может совмещал ься ь выражении со значениехт типа Щ„iit il a) r i t 6) LI " B) 11 r) nZ-e 10. Когда необходимо применять приведение липов’ 11. Напишите программу, колорая находит все простые чис та в тпапазотп от 1 до 100 12. Самостояте тьно перепишите программе предпазпаченнуто ня вывода таблицы истинности (проект 2-2), таким образом чтобы в ней вместо cscape-noc тсдоватс тьностеп испотьзова птсь копирующиеся строковые титсралы с в юженными знаками табу тянии и симво тами новой строки
глава Управляющие операторы □ Ввод символов с клавиатуры □ Детальное рассмотрение операторов if и for □ Оператор switch □ Цикл while □ Использование цикла do-while □ Оператор break □ Использование оператора continue □ Оператор goto
Ввод символов с клавиатуры 95 В этой главе будут рассмотрены операторы, которые управляют процессом выполне- ния программы. Существуют три категории управляющих операторов — операторы выбора (if и switch), итерационные операторы (for, while, do-while и foreach) и операторы перехода (break, continue, goto и return). Здесь мы подробно расскажем обо всех перечисленных управляющих операторах, за исключением опе- раторов return и foreacn, которые будут описаны далее в этой книге. Ввод символов с клавиатуры Прежде чем перейти к изучению управляющих операторов, сделаем небольшое oi отупление и расскажем о том, каким образом в C# можно работать в интерактивном режиме. В предыдущих главах нашей, книги рассматривались программы, только выводящие информацию на экран, теперь же в них будет предусмотрено и получение данных от пользователя. Для чтения символов с клавиатуры необходимо вызвать метод Console. Read () , который ожидает ввода символа с клавиатуры, а затем возвращает результат. Результат возвращается как целочисленное значение, поэтому он должен быть преобразован в пи, char, чтобы переменной был присвоен этот же тип. По умолчанию строка, вводимая с клавиатуры, помещается в буфер, и для того чтобы введенные символы были переданы программе, нужно нажать клавишу [Enter], Представленная ниже программа демонстрирует, как считываются символы, введенные с клавиатуры. / ''В программе демонстрируется считывание символов, вводимых с клавиатуры. □song System; Bu-ass Kb In { public static void Main() { char ch; Console-Write ("Нажмите символьную клавишу, после чего нажмите"-* " клавишу ENTER: "); ______ __________ _______________ ___—| Чтение символа, вводимого с клавиатуры. ch - (char) Console.Read();// Считывание символа. Console.WriteLine("Вы ввели символ: " + ch); Ниже приведен пример работы программы. •Нажмите символьную клавишу, затем нажмите клавишу ENTER: аы ввели символ р Иногда у программистов вызывает недовольство то, что Строка ввода метода Read () помешается в буфер, поскольку это немного усложняет работу. Ведь при нажатии клавиши [Enter] во входной! поток вводится последовательность символов перехода на новую строку, и эти символы остаются в буфере ввода до тех пор, пока не будут считаны. Поэтому при работе с некоторыми приложениями (в этой главе вам встретится такой пример) действительно возникает неудобство — перед новым вво- дом приходится сначала удалять эти символы из буфера посредством их считывания.
Глава 3. Управлнющив операторы Минутный практикум I. Как считыиаеи’н символ. кислен кын с клавиатуры? 2. Чю означаем помеihchiic и 1 роки в буфер? Оператор if Oncpaiop : г уже рассмачрнплгея в главе I. генерь мы расскажем о нем подробнее. Синтаксис оператора if е ею шорой частью , .,е выглядит следующим образом: д 1' I eo.'Idl С 1 Ш!) s Cd Г L*'C С : „ . dCdleiiid.-iC; Здесь при выполнении условия оператора i ;. инициируется только один оператор, если же условие нс выполняется, то также инициализируется только один оператор, от нос я щи вся к к иочевомх слову . - . .11я oncpaiора : : нс обязательно указывать ei о вторую часть <•.- К оператору । _ - мог уч относиться также блоки опера- торов. Общий синтаксис оператора нс ттсно.тыотзанием блоков операторов выглядит следующим образом: (cundnian) д ? а сю j 1 о 11 и с т » С. < I [ с* р <3 '1' С1 Р О Если условное выражение (г щ . . ) принимает значение то выполняется последовательность операторов, соответствующих оператору и. если же условное выражение принимает значение у3. то при наличии оператора else выполняется последовательность операторов, соответствующая части е .ле. Обе част оператора, i! и el so, ис могут выполняться одновременно. Условное выражение, управляющее оператором ы должно возвращать результат тина Щ: ,. Для демоны рации работы оператора и разработаем программу простои итры на учадывание. которая предназначена для маленьких детей. В первой версии игры ттротрамма просит трока угадать букву и диапазоне от А до Z. Если игрок верно выбирает букву па клавиатуре, программа выводит на экран сообщение - ’ГТрапиль- : *. Ниже приведен текст этой программы. ,Тр,к'1-ая игра "Угадай г.ьу " . г.. ...а;,- алОа.к./та, , " :...р " а кроз раине .") ; I. Для чтения символа необходимо вызвать метод Console . гсаа (;. 2. При вводе строка помешается в буфер, поэтому необходимо нажать клавишу | Enter], прежде чем введенные символы будут переданы программе.
Оператор if 97 Console . WriteLine (’’Введите символ. " = (char) Console.Read();//Считывает символ, введенный с клавиатуры. ,f(Ch answer) Console.WriteLine(”** Правильно Программа просит ввести символ с клавиатуры и считывает его. Далее используется оператор if для проверки соответствия введенного символа значению переменной erswer. в данном случае букве К. Если игрок вводит букву К, на экран выводится сообщение ** правильно При вводе должны использоваться символы верхнего регистра. Игровую программу можно усовершенствовать. 13 следующей Персии используется вторая часть оператора if-else для вывода сообщения о неверной попытке. // Программа "Угадай букву" версия . using System; puolic static void Main() { char ch, answer = ’K’; Console .WriteLine ("Угадайте букву алфавита, \"спрятанную \ " в программе. Console.WriteLine("Введите символ."); ch = (char) Console.Read();// Считывает символ, введенный с клавиатуры. if (ch answer) Console. WriteLine (’’** Правильно else Console.WriteLine("...He угадали..”); данных после того, е. (То есть вложен- дсйствия требуется Вложенные операторы if Вложенный оператор if используется для дальнейшей проверки как условие предыдущего оператора if принимает значение tru НЫ11 оператор применяется в тех случаях, когда для выполнения соблюдение сразу нескольких условий, которые нс могут быт ь указаны в одном условном выражении.) В программировании вложенные операторы if используются Достаточно часто. Необходимо помнить, что во вложенных операторах if-else вторая часть оператора (else) всегда относится к ближайшему оператору if, за Условным выражением которого следует оператор ; или блок операторов. В приве- денном ниже примере указаны два ключевых слова else, относящихся к разным
Глава 3. Управляющие операторы В комментарии указано, что последнее ключевое слово else не ассоциировано с оператором if (j < 20), поскольку оно находится за блоком кода, относящимся к оисраюру if(i == Ю). Го есть последнее ключевое слово else относится к оператору if (1 == 10), а нс к оператору сf (] < 20), хотя этот оператор расположен ближе к ключевому слову. Вложенный оператор if можно использовать для дальнейшего усовершенствования программы «Угадай букву». В новой версии обеспечивается обратная связь с игро- ком — программа подсказывает, в каком направлении следует искать букву. // Программа "Угадай букву" версия N*3 using System; class Guess3 { public static void Main() { char ch, answer — *K’; Console.WriceLine ( Угадайте букву алфавита, \"спрятанну1с\" в программе."); Console.WriteLine("Введите символ.-); oh (char) Console. Read () ;// Считывание символа. if (ch — answer) Console . WriteLine ("’« Правильно ***"); else { Console.Write("Попытайтесь поискать ");___________________ Вложенный оператор if- ] // Вложенный оператор if. if (ch < answer) Console.WriteLine("выше no алфавиту."); else Console.WriteLine("ниже по алфавиту."); ) Пример выполнения программы показан ниже. Угадайте букву алфавита, "спрятанную" в программе. Введите символ. В Попытайтесь поискать выше в алфавите. Цепочка операторов if-else-if В программировании часто используется цепочка операторов if-else-if — конст- рукция, состоящая из вложенных операторов if. Выглядит она следующим образом: if(condition) statement; else it (condition} statement; else it (condition} statement; else staCement;
Г)перат°Р it 99 успоиные выражения оцениваются сверху вниз. Как только найдено условие, при- нимаюиюе значение true, выполняется ассоциированный с этим условием оператор (statement), а остальная часть цепочки пропускается. Если ни одно из условий не принимает значение true, то выполняется последний оператор else, который можно рассматривать как оператор по умолчанию. Если же последний оператор else отсутствует, а все условные выражения принимают значение false, то программа не выполняет никаких действий. В следующей программе показано использование цепочки операторов ii-else-if. II в программе демонстрируется использование цепочки операторов if-else-if. using System; class Ladder { public static void Main() { for(x-0;x<6;x if(x-=l) Console . WriteLine ("Значение переменной x равно I.’’); else if(x—2) Console.WriteLine("Значение переменной x равно 2."); else if(x--3) Console.WriteLine ("Значение переменной x равно 3.’’); else if (x==4) Console . WriteLine ("Значение переменной x равно 4.’’); else Console.WriteLine("Значение переменной x не входит в "диапазон значений от 1 до 4."); Этот оператор выполняется по умолчанию. Программа выводит следующие строки: Значение Значение Значение Значение Значение Значение переменной переменной переменной переменной переменной переменной х не входит в диапазон от 1 до 4. х равно 1 х равно 2 х равно 3 х равно 4 х не входит в диапазон от 1 до 4. Как видите, оператор else выполняется только тогда, когда ни одно из предшест- вующих условных выражений оператора if не принимает значение true. Jp Минутный практикум to I- Какого типа должен быть результат условного выражения оператора if? * • э 8 J 2- С каким из операторов if всегда ассоциируется оператор else? 3. Что такое цепочка операторов if-else-if? ' Результат условного выражения оператора if должен иметь тип bool. -• Оператор else всегда ассоциируется с ближайшим оператором if, после блока операторов которого (или единственного оператора) он находится. 2- Цепочка операторов if-else-if является последовательностью вложенных операторов else-if.
100 Глава 3. Управляющие операторы Оператор switch Оператор switch является вторым оператором выбора в С#. Этот оператор позвотяет программе выбрать нужное действие из перечня вариантов. Принципы работы вложенных операторов if-else и операторов switch различаются причем во многих ситуациях использование оператора switch является более эффективным Оператор switch работает следующим образом: значение выражения последователь- но сравнивается е каждой константой из списка. При совпадении значения с одной из констант выполняется ассоциированная с ней ппспрпппптп-пил,. . н и последовательность операторов. Общая форма синтаксиса оператора switch следующая- switch(expression) < case constant 1: statement sequence break; case constant 2: statement sequence break; case constant 3: statement sequence break; default: statement sequence break; Выражение (expression) в операторе switch должно быть целочисленного типа (char, byte, short или int) либо типа string (который описан далее в этой книге). Выражения с плавающей точкой запрещены для использования в операторе switch. Очень часто в качестве выражения, управляющего оператором switch, используемся просто переменная. Константы (constant) ветвей case оператора swi ten должны быть литералами и иметь тип, совместимый с выражением. Кроме того, в одном блоке оператора switch они нс могут иметь одинаковые значения. Последовательность операторов (statement sequonce) относящихся к ветви de- fault, выполняется тогда, когда ни одна из констант ветвей case не соответствует выражению. Ветвь default нс обязательна. Если при ее отсутствии в операторе switch ни одна из констант нс соответствует выражению, то никакие действия нс будут выполнены. Если соответствие обнаружено, то выполняется последователь- ность операторов, ассоциированная с этой ветвью case, затем с помощью оператора break контроль над выполнением программы передастся конструкции, следующей за оператором switch. Приведем пример программы, демонстрирующей применение оператора switch. // В программе демонстрируется использование оператора switch using System; class SwitchDemo { public static void Main() { int i;
Оператор switch 101 for (i-0;i<10;i+ + ) switch(i) ( case 0: Console.WriteLine("Значение переменной i равно С."); break; case 1: Console.WriteLine("Значение переменной i равно 1."); break; case 2: Console.WriteLine("Значение переменной i равно 2."); break; case 3: Console.WriteLine("Значение переменной i равно 3."); break; case 4: Console.WriteLine("Значение переменной i равно 4."); break; default: Console.WriteLine("Значение переменной i больше или равно 5 break; Ниже показан результат выполнения этой программы. Значение переменной i равно 0. Значение переменной i равно 1. Значение переменной i равно 2. Значение переменной i равно 3. Значение переменной i равно 4. Значение переменной 1 больше или равно 5. Значение переменной i больше или равно 5. Значение переменной i больше или равно 5. Значение переменной i больше или равно 5. Значение переменной i больше или равно 5. Как видите, каждый раз при прохождении цикла выполняется оператор, ассоцииро- ванный с ветвью case, константа которой совпала со значением переменной i. Все Другие операторы пропускаются. Когда значение переменной i больше или равно 5, ни одна из констант ис соответствует этому значению, то есть в этом случае выполняется последовательность операторов, определенных для ветви default. предыдущем примере оператор switch управлялся переменной типа int. Уже говорилось, что управлять оператором switch можно с помощью выражения любого целочисленного типа. Следующая программа демонстрирует использование выраже- ния типа char и константы ветвей case, также имеющих тип char: II В программе для управлением оператором switch используется выражение, // имеющее тип char. using System; class SwitchDemo2 { public static void MainO { char ch; for(ch‘'A';ch <“ ’E';ch++)
Глава 3. Управляющие операторы switch(ch) { case ’A ’: Console.WriteLine("Значением переменной ch является символ A.”); break; case ’В’; Console. WriteLine ("Значением переменной ch является символ В.’’); break; case 'С’: Console.WriteLine("Значением переменной ch является символ С.”); break; case ’D': Console.WriteLine("Значением переменной ch является символ D."); break; case 'E’: Console.WriteLine("Значением переменной ch является символ E.”); break; } } } Результат выполнения этой программы показан ниже: Значением переменной ch является символ А. Значением переменной ch является символ В. Значением переменной ch является символ С. Значением переменной ch является символ D. Значением переменной ch является символ Е. Обратите внимание, что в данной, программе отсутствует ветвь default, которая не является обязательной и может быть пропущена, если в ней нет необходимости. Последовательность операторов, ассоциированная с одной ветвью case, не должна передавать управление программой следующей ветви case (это называется правилом запрещения передачи управления программой между ветвями case), такое действие в C# рассматривается как ошибка. Во избежание этого последовательности операторов ветвей case заканчиваются оператором break. (Предотвратить передачу управления можно и другими способами, но чаще всего для этого используется именно оператор break.) Если оператор break расположен после последовательности операторов ветви case, это приводит к выходу из всего оператора switch и передаче управления программой следующей конструкции за пределами switch. Оператор default также должен следовать этому правилу, и поэтому обычно заканчивается оператором break. Недопустимо, чтобы одна последовательность операторов ветви case передавала управ- ление программой операторам другой, ветви, но две и более ветви case могут быть связаны с одной и той же последовательностью кода, как показано в следующем примере: { case 2: case 3: Эти ветви case вызывают один и тот же оператор. Console.WriteLine("Переменная i имеет значение 1, 2 или 3"); break; case 4: Console.WriteLine("Значение переменной i равно 4") ; break; 1 Если переменная i имеет значение 1, 2 или 3, то выполняется первый оператор WriteLine () ;. Если значение переменной равно 4, то выполняется второй оператор WriteLine () ;. Возможность использования одной последовательности операторов несколькими ветвями case не нарушает правило, запрещающее передачу управления
Оператор switch 103 программой между ветвями case, и весьма часто используется в программировании. 'Гак, в следующей программе она используется для разделения букв алфавита па (ласпые и согласные. (Для корректной работы программы следует вводить символы нижнего регистра.) ;/ В программе демонстрируется использование несколькими ветвями case одной / > последовательности операторов для разделения букв на гласные и сох’ласные. using System; class VowelsAndConsonants { public static void Main() ( char ch; Console .Write ("Введите букву: ch - (char) Console.Read(); switch(ch) case case case case case case a e о У Console.WriteLine("Это гласная буква.’’); break; default: Console.WriteLine("Это break; согласная буква."); Если бы этот пример был написан без использования вышеназванной технологии, то запись одного и того же оператора WriteLine () ; необходимо было бы повторить шесть раз. ^-Ответы профессионала-- ——••— ----------------------------— - Вопрос. В каких случаях вместо оператора switch нужно использовать цепочку операторов if-else-if? Ответ. Общее правило таково: цепочка операторов if-else-if используется в тех случаях, когда выражение условия может иметь много значений (то есть когда в выражении условия удобнее применить оператор сравнения if (j < 0) ) или требуется сравнение нескольких переменных. В качестве примера рассмотрим сле- дующую цепочку операторов: if (х < 10) // ... else if(y ’ - 0) // ... else if(’done) // ... Такую последовательность нельзя записать с помощью оператора switch, поскольку все три условия основаны на различных переменных и различных типах. Также цепочка операторов if-else-if используется для проверки значений с плавающей точкой или для проверки объектов других типов, которые нельзя применять в выражении оператора switch.
104 Глава 3. Управляющие операторы Вложенный оператор switch Оператор switch может быть частью внешнего оператора switch. Внутренний оператор switch называется вложенным. Константы ветвей case внутреннего и внешнего операторов switch могут иметь одинаковые значения, и это нс вызовет конфликта при выполнении программы. Например, следующий фрагмент кода в п о л не доп у сти м: switch(chl) { case ’А*: Console.WriteLine("Эта константа ’А' является частью "+ " внешнего оператора switch."); switch(ch2) { case 1 2 3A ’ : Console.WriteLine("Эта константа 'А' является частью ”+ "внутреннего оператора switch."); break; case 'В*: // ... } // Закрывающая скобка внутреннего оператора switch, break; case ’В’: // ... Вопрос. В языках С, C++ и Java последовательность операторов одной ветви case может передавать управление программой последовательности операторов следующей ветви case. Почему это нс разрешено с С#? Ответ. В C# правило запрещения передачи управления программой между ветвями case принято по двум причинам. Во-первых, это правило позволяет компи- лятору организовать порядок перечисления операторов ветвей case наиболее опти- мальным образом. Это было бы невозможно, если бы последовательность операторов одной ветви case могла передавать управление следующей последовательности операторов. Во-вторых, в C# каждая последовательность операторов должна закан- чиваться оператором break, что также нс позволяет передавать управление програм- мой между ветвями case. g Минутный практикум j 1. Какого типа должно быть выражение, управляющее оператором switch? 3 2. Что происходит, когда выражение оператора switch совпадает со значением одной из констант ветви case? 3. Может ли последовательность операторов, ассоциированная с одной, ветвью case, передавать управление программой следующей ветви сазе? I. Выражение оператора switch должно быть целочисленного типа, либо типа string. 2. При совпадении проверяемого значения со значением константы выполняется последова- тельность операторов, ассоциированная с этой ветвью case. 3. Нет, поскольку с C# для оператора switch существует правило, запрещающее передачу управления между ветвями case.
Оператор switch 105 Проект 3-1. Построение простой справочной системы C# pj Help.cs В этом проекте мы построим простую справочную систему, которая выводит обитую форму синтаксиса для управляющих операторов С#. Программа выводит меню, содержащее список управляющих операторов, и ожидает, пока вы выберете один из них. После того, как выбор сделан, на экран выводится общая форма синтаксиса этого оператора. В первой версии программы будет доступна только информация о синтаксисе операторов if и switch. Общие формы синтаксиса других управляющих операторов будут добавлены в следующих проектах. Пошаговая инструкция I. Создайте файл и назовите его Help.cs. 2. Программа начинает работу с вывода на экран следующего меню: Справка по синтаксису: 1. Оператор if. 2. Оператор switch. Введите порядковый номер оператора: поэтому вам необходимо использовать такую последовательность операторов: Console .WriteLine ("Справка по синтаксису:"); Console.WriteLane(" 1. Оператор if."); Console-WriteLine(" 2. Оператор switch."); Console.Write("Введите порядковый номер оператора: "); 3. Программа получает информацию о выборе пользователя, вызывая метод Соп- cole. Read (), как показано ниже: choice - (char) Console.Read О ; 4. После получения информации о выборе пользователя программа использует оператор switch, представленный ниже, чтобы вывести на экран синтаксис выбранного оператора, switch (choic е) ( case ’1': Console.WriteLine ("Синтаксис оператора if:\n"); Console.WriteLine ("if(condition) statement;"); Console.WriteLine ("else statement;”); break; case ’2’: Console.WriteLine ("Синтаксис оператора switch:\n"); Console.WriteLine ("switch{expression) {"); Console .WriteLine (" case constant;”); Console .WriteLine (’’ Console.Wri teLine (" Console.WriteLine (” statement sequence ’’) ; break;"); // ..."); Console .WriteLine (" } " ) ; break; default: Console.Write("Порядковый номер указан неверно."); break;
106 Глава 3. Управляющие операторы Отмстим, что ветвь default перехватывает информацию о неправильно указан- ных порядковых номерах. Например, если пользователь введет значение 3, не совпадающее ни с одной из констанч операторе! case, то эго приведет к выпол- нению оператора, определенного для ветви default.. 5. Ниже представлен полный листинг программы Help.cs: /* Проект 3-1 Простая справочная система. */ using System; class Help { public static void Main() ( char choice; Console.WriteLine("Справка ко синтаксису:"); Console.WriteLine(" 1. Оператор if."); Console.WriteLine(" 2. Оператор switch.”); Console.Write("Введите порядковый номер оператора: "); choice “ (char) Console. Read (); Console.WriteLine("\n"); switch(choice) ( case ’1’: Console.WriteLine("Синтаксис оператора if:\n"); Console.WriteLine("if(condiCion) staCement;"); Console -WriteLine("else statement;"); break; case ’2’: Console.WriteLine("Синтаксис оператора switch:\n”); Console.WriteLine("switch(expression) • ") ; Console-WriteLine(" case constant:"); Console-WriteLine(" statement sequence "); Console-WriteLine(" break;"); Console.WriteLine(" // ..."); Console . WriteLine (’’} ") ; break; default: Console.Write("Порядковый номер указан неверно."); break; } i } Ниже приведен пример выполнения этой программы. Справка но синтаксису: 1. Оператор if. 2. Оператор switch. Введите порядковый номер оператора: 1 Синтаксис оператора if: if(condition) statement; else statement;
Цикл for 107 Цикл for С нерпой главы этой книги в программах используется простейшая форма цикла : Теперь мы расскажем обо всех возможностях этого оператора, и вы увидите его мощь и гибкость. Начнем с рассмотрения основ никла for и его наиболее традиционных форм. Общий синтаксис цикла for для повторения одного оператора выглядит так: юг(initialization; condition; iteration) statement; Для повторения блока операторов используется цикл, запись которого отличается от предыдущей только наличием открывающей и закрывающей скобок, между которыми и находится блок выполняемых операторов. юг(initialization; condition; iteration) statement sequence При инициализации управляющей переменной цикла (initialization — первая со- ставляющая цикла), которая действует как счетчик для управления циклом, ей присваивается начальное значение. Для этого обычно используется оператор при- сваивания. Составляющая цикла condition — это выражение условия, имеющее тип от истинности которого (при проверке измененного значения управляющей переменной) зависит, будет ли выполнен еще один проход цикла. Составляющая цикла iteration — это выражение, определяющее величину (шаг), на которую будет изменяться значение контрольной переменной при каждом повторении цикла. Отметим, что эти три основных элемента цикла должны быть разделены точкой с запятой. Цикл for повторяется до тех пор, пока в результате проверки выражения условия будет возвращаться значение true. Если будет возвращено значение false, никл завершится и управление программой будет передано оператору, следующему за никлом for. В приведенной ниже программе цикл for используется для вывода результатов вычисления квадратных корней чисел от 1 до 99. Программа также выводит ошибки округления для каждого результата. . : Программа выводит результаты вычисления квадратных корней чисел // от 1 до 99. using System; class SqrRoot { public stacic void MainO { double num, sroot, rerr; for(num - 1.0;num < 100.0; num-rf) sroot = Math.Sqrt(num); Console .WriteLine ("Квадратный корень числа ” т num + " равен ’’ * sroot); // Вычисление ошибки округления, rerr - num - (sroot * sroot); Console.WriteLine("Ошибка округления равна " + rerr); Console.WriteLine() ;
108 Глава 3. Управляющие операторы Обратите внимание, что ошибка округления вычисляется следующим образом: воз- водится н квадрат значение квадратного корня числа, затем этот результат отнимается от первоначального значения. Следовательно, значение разности и будет составлять ошибку округления. Иногда ошибка округления будет происходить еще и при возведении в квадрат значения квадратного корня, го есть будет округляться сама ошибка округления. Этот пример показывает, что если в выражениях используются числа с плавающей точкой, то эти вычисления могут быть недостаточно точными. Переменная цикла может не только увеличивать, но и уменьшать свое значение, кроме того, контрольная переменная может изменяться на любую величину при каждом проходе цикла. Например, следующая программа выводит числа в диапазоне от I00 до -I00 с шагом уменьшения переменной цикла, равным 5: // Цикл с отрицательным шагом изменения переменной цикла. using System; class Pecrfor { public static void Main() { int x; for(x = 10C;x > -100;x — 5) •+ Console.WriteLine (x); Переменная цикла уменьшается на 5 при каждом прохождении цикла. Следует отмстить, что в цикле for выражение условия всегда проверяется в начале цикла. Это означает, что если изначально выражение условия принимает значение false, код внутри цикла не будет выполнен ни разу. Например, for(count - 10);count < 5;count++) x г- count;// Этот оператор выполняться не будет. Оператор этого цикла не будет выполняться никогда, поскольку его управляющая переменная count имеет значение больше 5 в начале цикла, что изначально делает условное выражение cctmt < 5 ложным. Некоторые варианты цикла for Цикл for является одним из самых разносторонних операторов. Например, в нем могут использоваться несколько управляющих переменных цикла. Рассмотрим сле- дующую п ро гр а м м у: // Использование запятых в управляющих частях цикла for. using System; class Comma { public static void Mdin() int i, j; forfi^O, j=10;i < j —) Console.WriteLine("Значения } } Обратите внимание, что цикл имеет 1 ◄------------две управляющие переменные. | переменных i и j : + i + "" + ]) ; Ниже показан результат выполнения этой программы.
цикл for 109 Значения переменных 1 и j 0 10 Значения переменных i и j 1 9 Значения переменных i и j 2 8 Значения переменных 1 и j 3 7 Значения переменных i и j 4 6 Здесь два оператора инициализации и два итерационных выражения разделены запятыми. Когда цикл начинает свою работу, обеим переменным присваиваются начальные значения. Каждый раз при про,хождении цикла значение переменной 1 увеличивается на единицу, а значение переменной ; уменьшается на единицу. Управление циклом tor с помощью нескольких переменных часто бывает удобным и упрощает алгоритмы, реализуемые с помощью этого цикла. В цикле вы можете использовать любое число операторов инициализации и итерационных операторов, но на практике применение более двух переменных для контроля никла for делает его слишком громоздким. Условие, управляющее циклом, может быть любым действительным выражением, которое принимает значение типа bool. Условие не обязательно должно включать управляющую переменную цикла. В следующем примере цикл продолжает выпол- няться до тех пор, пока пользователь не введет символ S. • ' Цикл выполняется до тех пор, пока не будет введен символ S. using System; class ForTest { public static void MainO { int i; Console.WriteLine("Для выхода из программы нажмите клавишу S.”); for(i -= 0; (char) Console. Read () != *S’;i++) Console.WriteLine (’’Проход № " + i); Недостающие части цикла for В C# можно создавать интересные варианты цикла for, оставляя пустыми вес или некоторые части инициализации, условия и итерации. Например, рассмотрим сле- дующую программу: // Некоторые определяющие части цикла for могут быть пустыми. using System; class Empty ( public static void Main() { int i, count = 0; for (i = 0; i < 10;) { -< count++; Итерационное выражение пропущено. Console. WriteLine ("Проход цикла N’ ’’ + count); i-»-+;// Увеличение управляющей переменной цикла на единицу.
Глава 3. Управляющие операторы Здесь итерационное выражение цикла for пропущено. Вместо этого контрольная переменная цикла i увеличивается на единицу внутри тела цикла. Это означает, что каждый раз при выполнении цикла выполняется проверка значения переменной 1 (значение переменной сравнивается со значением 10), но дальнейших действий не происходит. (Переменная count введена только для того, чтобы счет проходов цикла начинался с цифры I, а не 0.) Поскольку изменение переменной 1 происходит внутри тела цикла, он работает нормально, выводя на экран следующую информацию: Проход цикла N' 1 Проход цикла N* 2 Проход цикла N* 3 Проход цикла № 4 Проход цикла № 5 Проход цикла № 6 Проход цикла № 7 Проход цикла № 8 Проход цикла № 9 Проход цикла № 10 В следующем примере инициализирующее выражение также вынесено за пределы определяющей части цикла for. //В этой программе инициализирующее выражение также вынесено за // пределы определяющей части цикла for. using System; class Empty2 { public static void Main() { .... ......... . . , , ... ... int i, count - 0; Инициализирующее выражение вынесено — ——_—_—_—... — . за пределы цикла. i - 0; // Инициализирующее выражение находится вне цикла for. for(;i < 10;) { count++; Console.WriteLine("Проход № ” + count); i++;// Увеличение управляющей переменной цикла на единицу. В данной версии программы инициализация переменной! г происходит до начала цикла, и это выражение не является частью цикла. Однако чаще контрольная переменная цикла инициализируется внутри определяющей части цикла for. Обычно операция инициализации выносится за пределы цикла только в тех случаях, когда начальное значение управляющей переменной является результатом вычисления сложного выражения, которое нельзя разместить внутри оператора for. Бесконечный цикл Используя оператор for с пустым условным выражением, вы можете создать беско- нечный цикл. Например, в следующем фрагменте кода показан способ создания бесконечного цикла: for(;;) // Бесконечный цикл. { // ... i
Объявление управляющих переменных цикла внутри цикла for 111 Такой цикл будет работать вечно. Хотя для выполнения некоторых задач программиро- вания (например, для реализации командных процессоров операционной системы) необходимы бесконечные циклы, существуют специальные средства, при помощи которых можно прекратить выполнение большинства таких циклов. В конце этой главы мы расскажем, как можно остановить бесконечный цикл. (Подсказка: это выполняется с помощью оператора break.) Циклы, не имеющие тела В C# тело цикла for (или любого другого цикла) может быть пустым. Это возможно, потому что пулевой оператор синтаксически является действительным. Циклы без 1сла используются достаточно часто. Например, в следующей программе такой цикл применяется для суммирования чисел от I до 5. .• Тело цикла может быть пустым. using System; class Empty3 { public static void Main() { int i; int sum = 0; // Вычисляется сумма чисел от 1 до 5. for(i = l;i О 5;surc +— i++) ; этом цикле тело отсутствует!^ Console.WriteLine("Сумма равна: ” + sum); } Ниже показан результат выполнения этой программы. Сумма равна 15 Отметим, что вычисление суммы производится полностью в пределах оператора for, следовательно, в теле оператора нет необходимости. Особое внимание обратите на итерационное выражение sum += 1++ Такие операторы выглядят несколько непривычно, тем не менее, они часто исполь- зуются в профессионально написанных CS-программах. Описать работу приведен- ного выше оператора можно следующим образом: «добавить к переменной зи значение переменной эта плюс значение переменной i, затем увеличить значение переменной i на единицу». То есть предыдущий оператор аналогичен последователь- ности операторов sum = sum + i; Объявление управляющих переменных цикла внутри цикла for Очень часто управляющая никлом for переменная необходима только для самого цикла и больше нигде не используется. В таких случаях можно объявлять переменную
112 Глава 3. Управляющие операторы внутри инициализирующей части цикла. Например, представленная ниже программа вычисляет как сумму, так и факториал чисел от I до 5. В этой программе управляющая переменная i объявляется внутри цикла for. // В программе управляющая переменная объявляется внутри // инициализационной части цикла for. using System; class ForVar ( public static void Kain() ( int sum = C; int fact = 1; I/ Вычисление суммы и факториала чисел от 1 до 5. [Переменная i объявляется forfint 1 - 1; i <- 5;i++) f •*-----------------------------j внутри оператора for. sum +- i;// Областью видимости переменной i является весь цикл, fact »= i; } // В этом месте программы переменная i не видна. Console.WriteLine("Сумма чисел от 1 до 5 = " + sum); Console.WriteLine("Факториал числа 5 - " + fact); 1 } При объявлении переменной внутри цикла for ее область видимости ограничена рамками цикла (то есть область видимости переменной ограничена циклом for), за его пределами переменная прекращает свое существование. Следовательно, в преды- дущем примере переменная i недоступна за пределами цикла. Если вам необходимо использовать управляющую переменную цикла в другом месте программы, то объя- вите эту переменную за пределами цикла for. Прежде чем перейти к изучению следующей темы, потренируйтесь в использовании различных вариантов цикла for. Вы увидите, что он имеет огромные возможности. Минутный практикум г G)\ 2. 3. Могут ли части оператора for быть пустыми? Напишите синтаксис бесконечного цикла for. Где заканчивается область видимости переменной, объявленной в пределах оператора for? 1. Да, все три части цикла for — инициализирующая, часть условия и итерационная — могут быть пустыми. 2. for(;;). 3. Область видимости переменной, которая объявлена в пределах оператора for, ограничена рамками цикла. За пределами цикла эта переменная неизвестна.
цикл while 113 цикл while Цикл while также широко применяется в CS-программах. Его синтаксис выглядит следующим образом: while (condition) statement; Здесь statement может быть одиночным оператором или блоком операторов, а condition является условием, которое управляет циклом, и может быть любым действительным булевым выражением. Оператор выполняется в том случае, если условие принимает значение true. Если же условие принимает значение false, управление программой передается строке кода, которая следует сразу за циклом. Приведем пример программы, в которой цикл while используется для вывода на экран символов латинского алфавита. /,’ В программе демонстрируется использование цикла while. using System; class WhileDemo ( public static void Main() { char ch; // Использование цикла while для вывода на экран символов // латинского алфавита, ch = 'а'; while(ch <= 'z') ( Console.Write(ch+ " "); ch++ ; } Здесь символ а присваивается переменной ch в качестве начального значения. Каждый раз при прохождении цикла значение переменной ch выводится на экран, ’а затем увеличивается на единицу. Этот процесс продолжается до тех пор, пока значение переменной ch не превысит z. Как и цикл for, цикл while проверяет условие в начале цикла, и это означает, что код цикла может вообще не выполняться. В приведенной ниже программе демонст- рируется использование цикла while для вычисления целочисленной степени числа 2. '' В программе используется цикл while для вычисления целочисленной // степени числа 2. using System; class Power { public static void Main() ( int e; int result; forfint i=C;i < 10;i++) ( result — 1; e - i; while(e > 0) ( result 2;
Глава 3. Управляющие операторы Console. WriteLine ("2 в " т i 4- "-й степени - " + result); } } } Результат выполнения этой програмхмы показан ниже. 2 в 0-й степени - 1 2 в 1-й степени - 2 2 в 2-й степени — 4 2 в 3-й степени — 8 2 в 4-й степени = 16 2 в 5-й степени = 32 2 в 6-й степени = 64 2 в 7-й степени = 128 2 в 8~й степени •= 256 2 в 9-й степени ~ 512 Заметьте, что цикл while выполняется только в том случае, если значение перемен- ной е больше 0. Поскольку при первом выполнении цикла for значение переменной 1, а значит и переменной е, будет равно 0, цикл while выполняться не будет. Цикл do-while В завершение этой темы рассмотрим цикл do-wile. В отличие от циклов for и while, где условие проверяется в самом начале, в цикле do-while условие проверяется в конце цикла. Это означает, что цикл do-while всегда выполняется как минимум один раз. Синтаксис цикла do-while следующий: do { statements; } while (condition) ; При наличии в цикле только одного оператора фигурные скобки нс обязательны, но их часто используют для улучшения читабельности конструкции цикла dc-while, поскольку его легко спутать с циклом while. Если при проверке условие принимает значение true, цикл do-while выполняется еще раз. В следующей программе цикл продолжается до тех пор, пока пользователь нс введет символ q. // В программе демонстрируется использование цикла do-while. using System; class DWDemo { public static void Main() { char ch; do { Console.Write("Введите символ, после чего нажмите ” клавишу ENTER: "); ch = (char) Console.Read();//Считывание символа. } while(ch ! = 'q'); )
цикл do-while 115 Цспользуя цикл do-while, мы можем усовершенствовать разработанную в начале павы программу «Угадай букву». Теперь программа будет выполняться до тех пор, пока буква не будет угадана. Программа "Угадай Сукну" версия N*4. using System; -_ass Guess4 { public static void MainO \ char ch, answer 'K'; do { Console .WriteLine ("Угадайте букву алфавита, \"спрятанную!" "в программе. "); Console.WriteLine("Введите символ: "); ,/ В цикле считывается символ, не игнорируются If escape-последовательности \п и \г, соответствующие клавише ENTER, do { ch (char) Console . Read ();/7 Считывание символа. } while(ch *\n' ch — *\r*); if (ch =- answer) Console.WriteLine ("** Правильно ; else { Console.Write("Попытайтесь поискать ’’) ; if(ch < answer) Console-WriteLine("выше в алфавите."); else Console.WriteLine("ниже в алфавите.\n"); } while(answer I- ch); Ниже показан один из возможных результатов работы программы: Угадайте букву алфавита, "спрятанную" в программе. Введите символ: А Попытайтесь поискать выше в алфавите. Угадайте букву алфавита, "спрятанную" в программе. Введите символ: Z Попытайтесь поискать ниже в алфавите. -йвдайте букву алфавита, "спрятанную" в программе. Поедите символ: К Правильно ** Обратите внимание на интересную особенность этой программы. Представленный ниже цикл do-while считывает вводимый символ, пропуская символ возврата каретки или новой строки, которые могут быть во входном потоке. ' : В цикле считывается символ, но игнорируются ' ' езсаре-иослеловательности \п и \г, соответствующие клавише [Enter] со { ch - (char) Console.Read() ;// Считывание символа. whale (ch '\i,' ch '\r'); Этот цикл был введен в программу именно хтя того, чтобы она игнорировала ненужные символы. Как уже говорилось ранее, при вводе данных с клавиатуры строка ввода помешается в буфер, поэтому прежде чем символы будет переданы программе, необходимо нажать клавишу [Enter], При этом генерируются символы возврата
Глава 3. Управляющие операторы каретки и новой строки, которые остаются во входном буфере. Цикл do-while отбрасывает эти символы и продолжает считывание до тех пор, пока не встретится какой-либо другой символ. Вопрос. Циклы обладают очень большой гибкостью. Как правильно выбрать цикл для определенной задачи? Ответ. Используйте цикл for. когда известно число повторений цикла, и цикл do-while, когда необходимо хотя бы одно прохождение цикла. Применение цикла while наиболее эффективно втех случаях, когда число повторений цикла неизвестно. Минутный практикум I. Каково основное различие между циклами while и do-while? 2. Справедливо ли утверждение, что условие, управляющее циклом while, может быть любого типа? Проект 3-2. Совершенствование справочной системы C# j7j Help2.cs В этом проекте расширяется справочная система С#, созданная в проекте 3-1, — добавляется синтаксис циклов for, while и do-while. Программа также проверяет правильность выбора пользователя, продолжая выполнение цикла до тех пор, пока не будет введен корректный символ. Пошаговая инструкция 1. Скопируйте код файла Help.cs и новый файл под названием Help2.cs. 2. Измените фрагмент программы, выводящей на экран меню выбора, так, чтобы использовался представленный ниже цикл. do { Console.WriteLine("Справка по синтаксису: "); Console.WriteLine(" 1. Оператор if"); Console.WriteLine(" 2. Оператор switch"); Console.WriteLine(" 3. Цикл for"); Console.WriteLine(" 4. Цикл while"}; Console.WriteLine(" 5. Цикл do-while\n"); Console.Write("Введите порядковый номер оператора или цикла: "); do { choice - (char) Console. Read () ; } while (choice -= '\n' . choice ~ '\r'); } while( choice < '1' I choice > '51 2); 1. Цикл while проверяет свое условие в начале цикла. Цикл do-while проверяет свое условие в конце цикла. Следовательно, цикл do-while всегда выполняется как минимум один раз. 2. Нет. Условие должно иметь тип bool.
цикл do-while II/ Здесь, как и в предыдущей программе, никл do-while используется для того, чтобы игнорировать символы возврата каретки и новой строки, которые могут присутствовать во входном потоке. После произведенных изменений программа будет выполнять цикл и выводить на экран меню до тех пор, пока пользователь не введет корректный символ •— цифру из диапазона от 1 до 5. 3 Расширьте оператор switch, включив в него синтаксис циклов for, while и -while, как это показано ниже. switch(choice) ' case ’1 ’ : Console.WriteLine("Оператор 1f:\n”); Console .WriteLine ("if (condition) statement; " ) ; Console.WriteLine("else statement;"); Ьгеак; case '2': Console.WriteLine("Оператор switch:\n"); Console.WriteLine("switch(expression) (") ; Console.WriteLine(" case constant:"); Console.WriteLine(" statement sequence"); Console.WriteLine(" break;"); Console.WriteLine(" // ...’’); Console .WriteLine (’’} ") ; break; case ’3’: Console.WriteLine("Цикл for:\n"); Console.Write("for(init;condition;iteration)") ; Console.WriteLine (" statement; ’’) ; break; case ' 4 ’ : Console.WriteLine("Цикл while:\n"); Console.WriteLine("while(condition) statement;"); break; case ’5’: Console-WriteLine ("Цикл! do-while: \n") ; Console-WriteLine("do {"); Console-WriteLine(" statement;"); Console-WriteLine("} while (condition);"); break; Отмстим, что в этой версии оператора switch отсутствует ветвь default. Цикл, предназначенный для отображения меню, гарантирует ввод пользователем дейст- вительного символа, поэтому ветвь default для обработки некорректного поль- зовательского ввода нс нужна. 4. Ниже представлен полный листинг программы Help2.cs: /* Проект 3-2. Улучшенная справочная система, использующая для обработки пользовательского ввода цикл while. */ using System; class Help2 ( public static void Main() { char choice; do {
I I о Глава 3. Управляющие операторы Console. WriteLine ( "Справка по синтаксису: Console-WriteLine(" 1. Оператор if"); Console-WriteLine(" 2. Оператор switch”); Console.WriteLine(" 3. Цикл for"); Console. WriteLine (’’ 4. Цикл while"); Console-WriteLine(" 5. Цикл do-while\n”); Console-Write("Введите порядковый номер оператора или цикла: ”); do { choice = (char) Console.Read(); } while(choice ’\n' 1 choice — ’\r'); } while( choice < *1’ I choice > ’5’); Console.WriteLine("\n"); switch(choice) { case ’1': Console.WriteLine("Оператор if:\n"); Console.WriteLine("if(condition) statement;”); Console.WriteLine("else statement;"); break; case '2’: Console.WriteLine("Оператор switch;\n"); Console.WriteLine("switch(expression) {"); Console-WriteLine (” case constant:"); Console.WriteLine(” statement sequence"); Console. WriteLine (’’ break;’’); Console-WriteLine (" // ..."); Console-WriteLine(”}"); break; case ’3’: Console.WriteLine("Цикл for:\n"); Console.Write("for(init;condition;iteration)"); Console-WriteLine(" statement;"); break; case ’4’: Console.WriteLine("Цикл while:\n"); Console.WriteLine("while(condition) statement;”); break; case ’5’: Console.WriteLine ("Цикл do-while:\n"); Console.WriteLine ("do {’’); Console. WriteLine (" statement; ’’) ; Console.WriteLine("} while (condition);"); break; Использование оператора break для выхода из Цикла Использование оператора break предоставляет возможность принудительного и немедленного выхода из цикла без выполнения оставшегося кода в теле цикла и проверки условия цикла. То есть цикл завершает свою работу, когда оператор break
Использование оператора break для выхода из цикла 119 встречается внутри никла, при этом управление программой передастся оператору, следующему за циклом. Приведем простой пример. / ' в программе демонстрируется использование оператора creak / для выхода из цикла. us-ng System; ass BreakOemo ( public static void Main() { Inc num; :. Цикл выполняется до тех пор, пока квадрат значения и переменной i не превысит значение переменной: num._______________________ tot (inс i- 0;i < num;^L4-+^ Использование оператора break для выхода из никла. if(i*i >- num) break;// Если выражение 1*1 >- 100 примет // значение true, будет выполнен оператор // break, что приведет к выходу из никла. Console.Write (i 4 " "); } Console.WriteLine("Цикл завершен."); Эта программа выведет следующую строку: С. 23456789 Цикл завершен. Цикл for должен повториться 100 раз, но выполнение оператора break приводит к досрочному прекращению работы цикла. Это происходит, когда значение переменной г, возведенное в квадрат, становится большим или равным значению переменной num. Оператор break может использоваться в любом цикле, включая преднамеренно опре- деленные программистом бесконечные циклы. Следующая программа просто считывает вводимые пользователем символы до тех пор, пока не будет нажата клавиша а. •’/ Программа считывает вводимые символы до тех пор, пока не будет нажата клавиша q. using System; class Break2 { public static void MainO { char ch; ;;) { --------------------------------------- (char) Console.Read();// Считывание символа. (ch — ’q') break;-4——— ~ ——— —————- J Работа э того бесконечно- го цикла прекращается с помощью оператора oreak. for ( ch if Console.WriteLine("Вы нажали клавишу ql."); Если оператор break используется внутри вложенных циклов, то он прекращает работу только внутреннего цикла. Например: / Использование оператора break во вложенных циклах. using System;
120 Глава 3. Управляющие операторы class ВгеакЗ { public static void Main() l int count. 1 - C; Console . WriteLine (”B npoi рамыс нолсчитыуается, сколько раз \n”-- ”был иьнюлнен каждый из циклов."); for (int 1--С ; i<3; i-i + ) count1~+; Console.WriteLine("Количество проходов внешнего цикла: " + countl); Console.Write(" Количество проходов внутреннего цикла: "); int 0-1; wh^le(t < ЮС) { ~f(t — 11) break; // Оператор прекратит выполнение цикла, // если переменная t примет значение 11. Console.Write(t + " "); t++ ; Console.WriteLine(); } Console-WriteLine("Внешний цикл завершен."); } } В результате выполнения этой программы на экран будет выведена следующая информация: В программе подсчитывается, сколько раз &ыл выполнен каждый из циклов. Количество проходов внешнего цикла: 1 Количество проходов внутреннего цикла: 12345678910 Количество проходов внешнего цикла: 2 Количество проходов внутреннего цикла: 123456789 10 Количество проходов внешнего цикла: 3 Количество проходов внутреннего цикла: 12345678910 Внешний цикл завершен. Как видите, использование оператора break во внутреннем цикле приводит к остановке только внутреннего цикла. Внешний цикл продолжает работать до полного завершения. профессионала О Вопрос. В Java операторы break и continue могу, использоваться с метками. Поддерживается ди такое использование операторов в С#? Ответ. Нет, разработчики C# нс наделили операторы break и continue такими возможностями, эти операторы в Ct работаю! так же, как в языках С и С+т~. Лело в том. что и языке Java нс поддерживается оператор дето, поддерживаемый в С«, поэтому в Java операторам break и continue необходимы дополнительные возможности для компенсации отсутствия оператора goto. Необходимо запомнить еще две особенности использования оператора break. Во- первых, в цикле может встречаться больше одного оператора break. Будьте внима- тельны, слишком большое количество операторов break и неправильно определенные
Оператор goto 121 условные выражения для этих операторов могут привести к некорректной работе программы. Во-вторых, оператор break, останавливающий работу оператора swi tch, влияет только на этот оператор switch и не влияет на работу внешних циклов. Использование оператора continue В C# существует возможность «заставить» цикл принудительно перейти к следующей итерации (следующему выполнению последовательности операторов, определенных в теле цикла). Это осуществляется с помощью оператора cor.tir.ae, который является дополнением к оператору break. Например, в следующей программе оператор continue используется для вывода на экран четных чисел из диапазона от 0 до I00: // В программе демонстрируется использование оператора continue. using System; class ContDcmo { public static void Main() { int i; // Вывод на экран четных чисел, // находящихся в диапазоне от 0 до 100. for(i - 0; К-10С;1 + т) { ____ ——’ " if((i%2) 0) continue; // Следующая итерация. Если значение переменной i нечетное, выполняется оператор continue. Console.WriteLine(i+ ” "); В этой программе на экран выводятся только четные числа, поскольку присвоение переменной i нечетного значения приведет к выполнению следующей итерации цикла с пропуском вызова метода WriteLine (). В циклах while и do-while использование оператора continue приводит к передаче управления непосредственно условному выражению, после чего выполнение цикла продолжается. В цикле for сначала выполняется итерационное выражение цикла, затем оценивается выражение условия, а уже после этого продолжается выполнение цикла. C# предоставляет возможность большого выбора циклов, которые удовлетворяют требованиям большинства приложений. Если же вы столкнетесь с редкой ситуацией, когда необходимо в определенный момент прекратить выполнение данной итерации Цикла, то сможете сделать это, используя оператор continue. Оператор goto Оператор goto в C# является оператором безусловного перехода. Если он встречается в коде, контроль над выполнением программы переходит в то место, которое указано оператором goto. Ранее многие программисты избегали использования этого оператора, поскольку в результате его применения программы становились неструктурными и слишком запутанными. Но иногда оператор goto может быть очень полезным. Необходимо признать, что в программировании обычно отсутствуют ситуации,
122 Глава 3. Управляющие операторы которые требуют обязательного использования этого оператора, скорее, он представ- ляет собой дополнение к инструментарию программиста. В нашей кише оператор got' рассматривается только в этой! главе и больше ншде не используется. Для оператора goto должна быть определена метка, причем ее определение должно быть выполнено в рамках того же метода, в котором она используется оператором goto. Метка представляет собой действительный идентификатор, за которым следует двоеточие. Например, запись цикла, который должен выполнить J00 итераций, при использовании оператора goto и метки выглядит следующим образом: loopl: <---------------------------------------------ч if (х < 100) goto loopl;—>|Контроль над управлением npoipa.MMofi переходит к метке 1оор'Г| Оператор goto можно эффективно использовать для выхода из глубоко вложенной процедуры. Вот простой пример его использования: /* В программе демонстрируется использование оператора goto. */ using Sysпет; class Use__goto { public static void Main() { int 1-0, j— 0, k.^0; for(i-0;i < 10;i++) { for(j-0;j < 10;j++ ) { for(k-0;k < 10;k++) { Console.WriteLine("i, j, k: " + i + ” " + j + ” " + k) ; if(k =- 3) goto stop; } stop: Console.WriteLine("Завершение выполнения цикла! i, j, k: "+i + ", " + j + " + k) ; Ниже представлен результат выполнения этой программы 1, 1, к: 0 0 0 1, j, k: С 0 1 1, j, к: 0 02 1, j, к: 0 0 3 Завершение выполнения цикла! i, j, к; 0, 0 3 Если бы мы исключили из программы оператор goto, то пришлось бы трижды использовать операторы if и break (то есть в данном примере оператор goto упрощает код). Этот пример приведен для того, чтобы вы могли представить себе ситуации, в которых применение оператора goto оправданно.
Оператор goto 12; Jp Минутный практикум I. Что происходит при выполнении оператора break в пределах цикла? { J Какие действия выполняются оператором continue? з Какой оператор в C# является оператором безусловного перехода? Проект 3-3. Завершение создания справочной системы C# Help3.cs В этом проекте мы завершаем создание справочной системы С#, над которой начали работать в предыдущих проектах. В последней версии добавляется синтаксис опера- торов break, continue и goto, а также создается возможность многократного обращения к справочной системе. Осуществляется это посредством добавления внешнего цикла, который выполняется до тех пор. пока пользователь не введет символ q. Пошаговая инструкция I. Скопируйте код файла Help2 .cs в новый файл, назовите этот файл Help3.cs. 2. Поместите весь код программы внутрь бесконечного цикла for. Для прерывания цикла используйте оператор break, который должен выполняться при вводе с клавиатуры символа q. Поскольку весь код программы помещен внутрь цикла for, выход из цикла будет приводить к завершению работы программы. 3. Измените цикл, предназначенный для отображения меню, следующим образом. 1. Оператор if”); 2. Оператор switch"); 3. Цикл for' ”> ; 4 . Цикл whi; le") ; 5. Цикл do-i while"); 6. Оператор break"); 7. Оператор continue" 8. Оператор goto\n"); do { Console-WriteLine("Справка по синтаксису: Console.WriteLine(” Console.WriteLine(" Console.WriteLine(” Console .WriteLine Console.WriteLine(" Console-WriteLine(" Console. WriteLine (’’ Console.WriteLine (’’ Console.WriteLine("Введите порядковый номер оператора или цикла: Console.WriteLine ("Для завершения работы программы введите символ q."); do { choice - (char) Console.Read(); } while(choice *\n' I choice =- ’\r’); } while ( choice < *1’ [ choice > ’8' & choice != 'q'); l. Выполнение оператора break в пределах цикла приводит к остановке цикла. Управление программой передается первой строке кода, следующей за циклом. 2. При выполнении оператора continue происходит немедленный переход управления к следующей итерации цикла без выполнения оставшегося кода. 3. В C# оператором безусловного перехода является оператор goto.
Глава 3. Управляющие операторы Обратите внимание, что теперь в этот цикл включена возможность выбора синтаксиса операторов break, continue и goto, кроме того, в нем учитывается возможность введения пользователем символа q. • 4. Расширьте оператор switch, чтобы он включал ветви case, отображающие синтаксис операторов break, cont inue и goto, как это показано ниже. case ’6': Console.WriteLine("Оператор bleak:\n"); * Console. WriteLine ("oreak;’’) ; break; case ' 7 ’ : Console,WriteLine("Оператор continue:\n"); < Console.WriteLine("continue;"); break; case ' 8 ' : Console.WriteLine("Оператор goto:\n"); Console.WriteLine (’’goto Label; ”) ; break; 5. Далее представлен полный листинг программы Help3 . cs. /‘ Проект 3-3 Заключительная версия справочной системы С#, к которой можно обращаться многократно. using System; class Help3 { public static void MainO ( char choice; for(;;) { do ( Console.WriteLine ("Справка no синтаксису: Console.WriteLine(" 1. Оператор if"); Console.WriteLine(" 2. Оператор switch") Console.WriteLine(" 3. Цикл for"); Console.WriteLine(" 4 . Цикл while’’) ; Console.WriteLine(" 5. Цикл do-while"); Console.WriteLine(” 6 . Оператор break"); Console.WriteLine(" 7 . Оператор continue Console.WriteLine (" 8. Оператор goto\n") Console.WriteLine("Введите порядковый номер оператора или цикла: Console. WriteLine ("Для завершения работы программы ” + "введите символ q.”); do { choice - (char) Console.Read(); } while (choice =--- ’\n’ i choice '\r'); } while( choice < '!' [ choice > *3’ & choice ! = *q'); if(choice *- •q' ) Console.WriteLine ("\n");
Оператор goto 1 switch (choice) { case * 1 ’ : Console.WriteLine("Оператор if:\n”); Console.WriteLine ("if (condition) statement; ”) ; Console.WriteLine("else statement;"); break; case ’ 2 ' : Console.WriteLine("Оператор switch:\n"); Console.WriteLine("switch(expression) f" >; Console.WriteLine(" case constant:"); Console.WriteLine(" statement sequence”); Console.WriteLine(" break;"); Console.WriteLine(" f) ...”); Console.Wri teLine(”•"); break; case ’3’: Console.WriteLine("Цикл for:\n"); Console .Write ("for (init; condition; iteration) ’’) ; Console .WriteLine (’’ statement; ” ) ; break; case ' 4 ’ : Console.WriteLine("Цикл while:\n"); Console-WriteLine("while(condition) statement;"); break; case ’5’: Console.WriteLine("Цикл do-while:\n"); Console.WriteLine("do {"); Console - Wri teLine (’’ statement; ’’) ; Console.WriteLine("} while (condition);"); break; case ’6’: Console.WriteLine("Оператор break:\n"); Console.WriteLine("break;"); break; case '7’: Console.WriteLine("Оператор continue:\n"); Console.WriteLine("continue;"); о r e a k; case ' 8 ' : Console.WriteLine("Оператор goto:\n"); Console.WriteLine ("goto laoel;"); break; } Console.WriteLine(); } J } Ниже показан один из возможных результатов выполнения программы. Справка по синтаксису: 1. Оператор if 2. Оператор switch 3. Цикл for 4. Цикл while 5. Цикл do-while 6. Оператор break 7. Оператор continue
126 ГлаваЗ. Управляющие операторы 8. Оператор goto Введите порядковый номер оператора или цикла: Для завершения работы программы введите символ q: 1 Оператор if: if (condition) statement; else statement; Сиравка по синтаксису: 1. Оператор i f 2. Оператор switch 3. Цикл for 4. Цикл while 5. Цикл do-while 6. Оператор break 7. Оператор continue 8. Оператор goto Введите порядковый номер оператора или цикла: Для завершения работы программы введите символ q: 6 Оператор break: break; Справка по синтаксису: 1. Оператор if 2. Оператор switch 3. Цикл for 4. Цикл while 5. Цикл ao-while б. Оператор break 7. Оператор continue 8. Оператор goto Введите порядковый номер оператора или цикла: Для завершения работы программы введите символ q: q Вложенные циклы Как видно из предыдущих примеров, один цикл можно вложить внутрь другого. Вложенные циклы используются для решения разнообразных задач и являются мощным инструментом в программировании. Поэтому, прежде чем завершить опи- сание операторов С#, рассмотрим еще один пример вложенного цикла. В следующей программе используется вложенный цикл for для нахождения делителей! чисел, находящихся в диапазоне от 2 до 100: / * В программе демонстрируется использование вложенного цикла для нахождения делителей целых чисел, находящихся в диапазоне от 2 до 100. */ using System; class FindFac { public static void Main() {
Вложенные циклы 127 forfint i=2;i <=- 100;i + + ) { Console. Wri te ("Делители числа " » i + "); for (inc j - 2;i < i;']*4-) 0) Console.Write (j Console.WriteLine(); Ниже представлена часть результатов выполнения программы. делители числа 2: делители числа 3: делители числа 4: 2 делители числа 5: Делители числа 6*. 2 3 делители числа 7: делители числа 8: 24 Делители числа 9: 3 Делители числа 10: 2 5 Делители числа 11: Делители числа 12: 2 3 4 б Делители числа 13: Делители числа 14: 27 Делители числа 15: 35 Делители числа 16: 2 4 8 Делители числа 17: Делители числа 18: 2369 Делители числа 19: Делители числа 20: 2 4 5 10 В этой программе при выполнении внешнего цикла значение переменной i увели- чивается от 2 до 100. Внутренний цикл успешно проверяет все значения от 2 до значения, которое в данный момент имеет переменная i, выводя на экран те из них, на которые значение переменной i делится без остатка.
128 Глава 3. Управляющие операторы Контрольные вопросы 1. Напишите программу, которая считывает символы, вводимые с клавиа- туры, до тех пор, пока не будет введен символ точки (.). Программа должна сосчитать количество введенных пробелов и вывести это число в конце программы. 2. Может ли последовательность кода одной ветви сазе передавать управ- ление программой другой ветви сазе в операторе switch? 3. Напишите синтаксис цепочки операторов if-else-if. 4. С каким оператором if ассоциирован последний оператор else в коде if (х < 10) iffy > 100) { iff! done) х z; else у ~ z; } else Console.WriteLine("Ошибка!");//К какому оператору if // принадлежит его часть else? 5. Запишите пример цикла for, в итерационном выражении которого число 1000 должно уменьшаться до 0 с шагом -2. 6. Является ли действительным следующий фрагмент кода: for(int i - 0; i < num; i++) sum +- i; count ~ i; 7. Какие действия выполняет оператор break? 8. Рассмотрите следующий фрагмент кода. Какая информация будет вы- ведена на экран после выполнения оператора break? ford - 0;r < 10; Щ + ) • while(running) { if (x<y) break; // ... f Console.WriteLine("Пикл while завершит свою работу."); Console.WriteLine("Цикл for завершил свою работу.’’); 9. Как будет оформлен вывод на экран значения переменной г? forfint 1 0;i < 10;i++) ( Console.Write (i + " if((i's2) -- 0) continue; Console.WriteLine();} 10. Итерационное выражение в цикле for нс всегда должно изменять кон- трольную переменную цикла на фиксированную величину. Контрольная переменная цикла может изменяться произвольно. Напишите программу, в которой! цикл for используется для вывода на экран геометрической! прогрессии 1, 2, 4, 8, 16, 32, 64 и так далее. |
Контрольные вопросы |« 11. Числовые значения символов нижнего регистра в коде ASCII отличают- ся от значений символов верхнего регистра на величину 32. Следова- тельно, для конвертирования символа нижнего регистра в символ верх- него регистра необходимо вычесть из его значения число 32. Используя эту информацию, напишите программу, которая читает символы, вво- димые с клавиатуры. Программа должна конвертировать все символы нижнего регистра в символы верхнего регистра и наоборот, выводя на экран результат. При этом все остальные символы остаются неизмен- ными. Программа должна прекращать работу, когда пользователь вводит символ точки (.). В завершение работы программа должна вывести информацию о количестве измененных символов.
Классы, объекты и методы □ Определение класса □ Создание объектов □ Создание методов □ Передача методу параметров □ Возвращение методом значений □ Использование конструкторов □ Оператор new □ Деструкторы и «сборка мусора» □ Ключевое слово this
Основные понятия класса 131 Прежде чем продолжать изучение языка С#, необходимо рассмотреть основную сфуктуру объектно-ориентированного программирования - - класс. Класс является (Ьупдаменто.м, на котором построен весь язык С#. В классе определены данные и код. работающий с этими данными. Это очень обширная и важная тема, иоэто.муьниматсльно a i\ чп io данную главу. Только поняв, что представляют собой классы, объекты и методы, вы сможете писать более сложные и совершенные профаммы на С». Основные понятия класса Весь активный процесс Сопрограммы происходит в пределах класса, поэтому с самого I начала в программах этой книги использовались классы. Понятно, что они биди самыми < простыми и вы смогли познакомиться только с малой частью их возможностей. Классы. ; рассматриваемые дальше, обладают гораздо большей! мощью. В этом раздце .мы под- ; робно остановимся на основных поня тиях класса. Класс является основой, для создания объектов. В классе определяются данные и i код, который работает с этими данными. Объекты являются экземплярами класса. J Непосредственно инициализация переменных в объекте (переменных кзе.шыяра) происходит в конструкторе. В классе могут быть определены несколько конструкто- ров, то есть класс является набором проектов, которые определяют, как строить обьект. Очень важно понимать разницу между классом и объектом: класс является ! логической абстракцией до чех нор, пока не будет создан обьект и нс появится j физическая реализация этого класса чз памяти компьютера. Методы и переменные, составляющие класс, называются членами класса. Общий синтаксис класса При определении класса объявляются данные, которые он содержит, и код, работаю- щий с этими данными. Самые простые классы могут содержать только коды и только данные, но в реальных программах классы включают обе эти составляющие. Данные содержатся в переменных экземпляра, которые определены кланом, а код содержится в методах. Важно с самого начала отмстить, что в C# определены несколько специфических разновидностей членов класса. Эго — переменные экзем- пляра, статические переменные, константы, методы, конструкторы, деструкторы, индексаторы, события, операторы и свойства. На данном этапе мы ограничимся описанием переменных экземпляра и методов, являющихся основными элементами класса. Позже в этой главе будут рассмотрены конструкторы и деструкторы О других пшах членов класса мы расскажем в следующих главах этой книги. При создании (определении) класса вначале указывается ключевое с.щщр class. Ниже представлен общий синтаксис определения класса, содержащий только пере- менные экземпляра и методы. / . объявление переменных экземпляра а с се ss t уро va г 1; access type var2; / / ... access type varN; // объявление методов access ret-type methodi(parameters){
13Z Глава 4. Классы, объекты и методы I/ тело метода } access ret-type method2(parameters){ // тело метода } access ret-type methods(parameters){ // тело метода Обрлите внимание, что объявление каждой переменной и метода начинается с подстановочного слова access, вместо которого объявляется модификатор, указы- ваюшнй, как осуществляется доступ к данному члену класса. Как уже говорилось в главе 1, использование модификатора private указывает на то, что члены класса доступны только для членов этого класса, а использование модификатора public — на то, что они открыты для кода, определенного за пределами данного класса. При определении члена класса модификатор указывать не обязательно, по умолчанию его отсутствие означает, что этот член класса имеет модификатор private. Члены класса, объявленные как private, могут использоваться только другими членами этого класса В программах данной главы вес члены класса определены как имеющие тип доступа public. Это означает, что они могут быть использованы любым другим кодом, даже определенным за пределами класса. Подробнее мы рассмотрим моди- фикаторы в следующих главах. Нс существует строгого синтаксического правила, ограничивающего сферу действий, выполняемых классом, или разнообразие сохраняемых данных, но в хорошо напи- санной программе класс определяет одну и только одну логическую сущность. Например, класс, в котором хранятся имена пользователей и их телефонные номера, как правило, не должен хранить данные фондовой биржи, информацию о среднеме- сячных температурах в Антарктиде, расписание авиарейсов либо другую логически не связанную с телефонным справочником информацию. .bi сих пор используемые нами классы имели только один метод — Main (). В этой главе мы рассмотрим, как создавать другие методы. Необходимо отмстить, что определение метода Main() не является неотъемлемой частью синтаксиса класса, этот метод необходим только в классе, с которого должна начинаться программа. Определение класса Любую тему легче всего изучать, используя практические примеры. Мы разработаем класс, который инкапсулирует информацию о транспортных средствах, таких как легковые автомобили, фургоны и грузовики, и назовем его Vehicle. Этот класс будет хранить данные о транспортных средствах — количество пассажиров, которое спо- собен перевозить автомобиль, объем топливного бака и расстояние (в милях), которое этот автомобиль может проехать, используя один галлон топлива. Первая версия класса vehicle, в котором определены три переменные экземпляра passengers, fuelcap и mpg, представлена ниже. Обратите внимание, что класс Vehicle не содержит никаких методов, то есть эта версия класса содержит только данные. (Далее в следующих версиях будут добавляться и методы.)
Основные понятия класса 133 class Vehicle { public int passengers; // Количество пассажиров. public int fuelcap; // Емкость топливного бака (в галлонах). public int mpg; // Расстояние (в милях), которое // автомобиль может проехать, используя опин // галлон топлива. । Принедснное выше определение переменных экземпляра можно рассматривать как обший способ объявления переменных. Синтаксис объявления переменных экземп- ляра таков: access type var-name; Здесь подстановочное слово access — это модификатор, слово type — тип пере- менной, а словосочетание var-name — имя переменной. Таким образом, синтаксис объявления переменной экземпляра аналогичен синтаксису объявления локальной переменной, за исключением модификатора. В классе Vehicle объявление перемен- ных начинается с модификатора public, это позволяет переменным быть доступны- ми для кода, определенного вне данного класса. С помощью ключевого слова class создастся новый тип данных. В нашем случае новый тип данных называется Vehicle. Это имя используется для объявления объектов типа Vehicle. Помните, что объявление класса с помощью ключевого слова class даст только описание типа, но физический объект при этом не создается. Поэтому выполнение приведенного выше кода не приведет к возникновению объек- тов типа Vehicle. Для того чтобы создать объект типа Vehicle, необходимо использовать оператор new, о котором мы расскажем далее в этой главе. Например, Vehicle minivan = new Vehicle О; // Создание объекта типа Vehicle, // который называется minivan. После выполнения этого оператора будет создан экземпляр класса Vehicle — объект minivan. При создании экземпляра класса создается объект, который содержит собственную копию каждой переменной экземпляра, определенной классом. Следовательно, ка- ждый объект класса Vehicle будет содержать свои собственные копии переменных экземпляра passengers, fuelcap и mpg. Для доступа к этим переменным использу- емся оператор точка (.). Этот оператор связывает имя объекта с именем члена класса. Приведем общий синтаксис данного оператора: object.member Как видите, имя объекта указывается слева от точки, а имя члена класса — справа. Например, для присваивания переменной fuelcap объекта minivan значения 16 используется следующий оператор: "-nivan. fuelcap - 16; Оператор точка (.) может использоваться для получения доступа как к переменным экземпляра, так и к методам. Ниже представлен весь код программы, в которой используется класс vehicle. /* В программе демонстрируется использование класса Vehicle. Назовите этот файл UseVehicle.cs
Глава 4. Классы, объекты и метода */ using System; class Vehicle { public int passengers; public int fuelcap; public int mpg; // Количество пассажиров. // Емкость топливного бака (в галлонах). // Расстояние (в милях), которое данный автомобиль // может проехать, используя один галлон топлива.} !/ В этом классе создается объект класса Vehicle. class VehicleDemo { public static void MainO Г ..——----- ——— ——--------—— _ Veh' clc mini van — new Vehicle () ; Зта строка кода создаст экземпляр класса Vehicle, который называется minivan. int range; I—————---------—————--------------- // Переменным объекта minivan присваиваются значения. minivan.passengers = 7; minivan . fuelcap - 16;-4- minivan.mpg - 21; Обратите внимание на использование оператора точка (.) для доступа к члену класса. // Вычисление максимального расстояния, которое может проехать // данный автомобиль, имея полный топливный бак. range - minivan.fuelcap * minivan.mpg; Console. WriteLine (’’Миниавтобус может перевезти ” 1 minivan.passengers r " пассажиров на расстояние ” + range + " миль."); } } В этой программе содержатся два класса Vehicle n VehicleDemo. Внутри класса VehicleDemo в методе Main() создается экземпляр класса Vehicle, называемый mm ni van. Затем код, определенный в методе Main (), получает доступ к переменным экземпляра (объекта minivan), присваивая нм значения и используя эти значения в вычислениях. Важно понимать, что классы vehicle и VehicleDemo являются различными, самостоятельными классами. Единственная связь между ними состоит в том. что в одном классе создается экземпляр другого класса. Хотя эти классы являются самостоятельными, код внутри класса VehicleDemo может иметь доступ к членам класса Vehicle, поскольку переменные класса Vehicle объявлены как public. Без указания этого модификатора доступ к переменным passengers, fuelcap и mpg мог бы осуществляться только кодом, определенным в пределах класса Vehicle, а класс VehicleDemo не имел бы возможности их использовать. Рассматриваемая программа была помещена в файл Usevehicle . cs, поэтому при ее компилировании будет создан файл Usevehicle .ехе. Оба класса, Vehicle и Vehi- cleDemo, автоматически станут частью исполняемого файла. Результат выполнения этой программы будет следующим: Миниавтобус может перевезти 7 пассажиров на расстояние 336 миль. Классы vehicle и VehicleDemo не обязательно должны находиться в одном и том же исходном файле. Можно поместить каждый класс в отдельный файл, назвав их, например. Vehicle . cs и VehicleDemo . cs. В этом случае нужно задать в командной строке команду esc Vehicle.cs VehicieDemo.es
Основные понятия класса 135 прп выполнении которой оба файла (оба класса, содержащиеся в них) будут нс только скомпилированы, но и объединены. При работе в интегрированной среде Visual C+HDE необходимо добавить к про- грамме оба эти файла, а затем их построить. Прежде чем перейти к изучению следующего материала, еще раз вернемся к основ- ному принципу создания объектов — каждый объект имеет собственные копии переменных экземпляра, определенных их классом. Следовательно, значения пере- менных одного объекта могут отличаться от значений переменных другого объекта. Например, если существуют два объекта Vehicle, то каждьп’| из них имеет свои собственные копии переменных passengers, fuelcap и mpg, значения которых могут различаться. Следующая программа демонстрирует использование этого принципа. В этой программе создаются два объекта класса Veh-c^e. using System; lass Vehicle { public int passengers; public int fuelcap; public int mpg; // Количество пассажиров. // Емкость топливного бака (в галлонах). // Расстояние (в милях), которое данный автомобиль // может проехать, используя один галлон топлива. В этом классе создаются два объекта класса Vehicle. cl«ss TwoVehicles { public static void Main() { Vehicle minivan new Vehicle(); Vehicle sportscar - new Vehicle ();^ ~ int rangel, range2; Помните, что переменные minivan и spot tear ссылаются на различные объекты. // Переменным объекта minivan присваиваются значения. minivan .passengers — 7; minivan.fuelcap - 16; minivan.mpg - 21; // Переменным объекта sportscar присваиваются значения. sportscar.passengers 2; sportscar.fuelcap - 14; sportscar.mpg - 12; // Вычисление максимального расстояния, которое может проехать // каждый из автомобилей, имея полный топливный бак. rangel - minivan.fuelcap * minivan.mpg; range2 - sportscar.fuelcap * sportscar.mpg; Console.WriteLine("Микроавтобус может перевезти " + minivan.passengers ” пассажиров на расстояние " + rangel + " миль."); Console.WriteLine("Спортивный автомобиль может перевезти " + sportscar. passengers -г " пассажира на расстояние \п" - range2 + " миль.");
। лава 4, Классы, объекты и методы Ниже приведен результат выполнения этой программы. Миниавтобус может перевезти 7 пассажиров на расстояние 336 миль. Спортивный автомобиль может перевезти 2 пассажиров на расстояние 168 миль. Данные объекта minivan полностью независимы отданных, содержащихся в объекте sportcar. На рис. 4.1 представлена структура этих объектов. Рис. 4.1. Переменные экземпляра одного объекта независимы от переменных экземпляра другого объекта Минутный практикум j I. Из каких компонентов состоит класс? Зх Л 1. Какой оператор используется для получения доступа к членам класса? 3. Собственные копии чего имеет каждый объект? Сак создаются объекты В предыдущей программе для объявления объекта типа Vehicle была использована следующая строка кода: Vehicle minivan - new Vehicle (); В ней выполняются два действия. Во-первых, объявляется переменная типа Vehicle, называемая minivan. Эта переменная не определяет объект (то есть не является непосредственно объектом), она только ссылается на объект. Во-вторых, при выпол- нении оператора new создается физический объект, а переменной minivan присваи- вается ссылка на этой объект. Следовательно, после выполнения данной строки кода переменная minivan будет ссылаться на объект типа Vehicle. Оператор new динамически (во время работы программы) выделяет память для объекта и возвращает ссылку на эту область памяти. То есть ссылка является адресом памяти, выделенной объекту оператором new. В дальнейшем эта ссылка хранится в перемен- ной. Следовательно, всем объектам в C# память должна выделяться динамически. Два шага, совмещенные в предыдущем операторе (строке кода), для большей нагляд- ности могут быть переписаны в двух операторах: Vehicle minivan; // Объявление ссылки на объект. minivan = new Vehicle(); // Выделение памяти для объекта Vehicle и возвращение // ссылки на этот объект переменной minivan. I. Класс состоит из кода и данных. 2- Для доступа к членам класса используется оператор точка (.). 3. Каждый объект имеет собственные копии переменных экземпляра.
Переменные ссылочного типа и оператор присваивания g первой строке кода переменная minivan объявляется как ссылка на объект типа vehicle. То есть переменная minivan может ссылаться на объект, но нс является самим объектом. На этом этапе данная переменная хранит значение nail, означаю- щее, что связь между ней и объектом отсутствует. В следующей строке кода создается новый объект класса Vehicle, а переменной minivan присваивается ссылка на этот объект, теперь переменная minivan связана с объектом. Поскольку доступ к объектам класса осуществляется с помощью ссылки, классы иначе называют ссылочными типами. Ключевое отличие между обычными и ссылоч- ными типами состоит в значениях, хранимых переменными каждого типа Перемен- ная обычного типа сама хранит значение. Например, после выполнения операторов in 1 X,- х - бес- переменная х хранит значение 10, поскольку х является переменной типа int, то есть переменной обычного типа. Но после выполнения оператора Vehicle minivan = new Vehicle(); переменная minivan самостоятельно не хранит объект, а хранит только ссылку на него. Переменные ссылочного типа и оператор присваивания При выполнении операции присваивания переменные ссылочного типа действуют иначе, чем переменные обычного типа (например, типа int). Когда значение одной переменной обычного типа вы присваиваете другой переменной обычного типа, то ситуация проста. Переменная, находящаяся слева от оператора присваивания, полу- чает копию значения переменной, указанной справа. Когда же значение одной переменной ссылочного типа вы присваиваете другой переменной, ситуация гораздо сложнее, поскольку вы присваиваете переменной ссылку на другой объект. Напри- мер, рассмотрим следующий фрагмент кода: Vehicle carl - new Vehicle (); Vehicle car2 - carl; Может показаться, что переменные carl и саг2 относятся к различным объектам, но это не так. На самом деле обе переменные carl и саг2 ссылаются на один и тот же объект. При присваивании переменной саг2 значения переменной carl, пере- менной саг2 присваивается ссылка на тот же объект, на который ссылается пере- менная carl. Следовательно, доступ к объекту можно осуществлять как с помощью переменной carl, так и с помощью переменной саг2. Например, после выполнения операции п р и с ва и ва н и я carl.mpg - 26; оба оператора WriteLine О выведут одинаковое значение — 26. Console.WriteLine(carl.mpg); Console.WriteLine(car2.mpg); Переменные carl и car2 не связаны каким-либо способом, хотя и ссылаются на один объект. Например, после выполнения следующего фрагмента кода переменная саг2 получает ссылку на объект, на который ссылается переменная сагЗ.
I лава 4. Классы, объекты и методы Vehicle carl - new Vehicle (); Vehicle car2 - carl; Vehicle car3 --- new Vehicle (); car2 — car3; • Теперь переменные car2 и сагЗ ссылаются /I на один и тот же объект. Объект, на который ссылается переменная carl, остается без изменений. Минутный практикум I. Что происходит, когда значение переменной ссылочного типа присваивает- ся другой! переменной ссылочного тина? 2. Предположим, что создан класс с именем MyClass. Покажите, как создать объект с именем об. 3. Чем отличаются переменные ссылочного типа от переменных обычного типа? Методы Как уже говорилось, переменные экземпляра и методы являются двумя основными составляющими классов. До этого момента в нашей программе класс Vehi.de содержал только данные и не содержал методов. Хотя классы, содержащие только данные, являются действительными, большинство классов содержит и методы. Методы — это подпрограммы, которые управляют данными, определенными в классе, и во многих случаях обеспечивают доступ к данным. Обычно остальные части программы взаимодействуют с классом при помощи его методов. Ме тод содержит один и более операторов. В профессионально написанном С#-коде каждый метод выполняет только одну задачу. В качестве имени метода может использоваться любой действительный идентификатор. Ключевые слова не могут быть именами методов, имя Main о является зарезервированным для метода, с которого начинается выполнение программы. В тексте программы методы обозначаются следующим образом: после имени метода следует пара круглых скобок. Например, если имя метода getvau, то при его вызове в программе он будет написан как netvar (). Такая форма написания применяется для того, чтобы в программах можно было различать имена переменных и методов. Общин синтаксис метода выглядит так: access ret-type name(parameter-list) { /l тело метода } Здесь подстановочное слово access — это модификатор, который! указывает, какие части вашей программы могут иметь доступ к методу. Как уже говорилось, модифи- катор нс является обязательным, если он отсутствует, метод доступен только в •. Когда значение переменной ссылочного типа присваивается другой переменной ссылочного типа, обе переменные будут ссылаться на один и тот же объект. При этом копирования объекта не происходит. - r’yClass ob new MyClassf); 3. Переменная обычного типа непосредственно хранит присвоенное ей значение, переменная ссылочного типа хранит только ссылку на объект.
Методы и» пределах класса, в котором он обьявлен. Пока мы будем объявлять нее методы как д L.c. поэтому они могут быть вызваны кодом из любого места программы. С ювосочстание ret-type в синтаксисе указывает тип данных, возвращаемых мето- дом- Эти могут быть как данные любого действительного типа, так и объекты любого, созданного вами класса. Если метод не возвращает никакого значения, он должен быть указан как имеющий тип void. Вместо слова name указывается имя метода. Это может быть любой действительный идентификатор, не повторяющий имена, которыми были названы другие члены класса в той же области видимости. После имени метода следует словосочетание parameter-list (список параметров) — по- слелова1ельность разделенных занятыми пар «тип-идентификатор». То есть с помо- щью параметров методу передаются из программы значения, гни которых обязатель- но должен быт ь указан. Параметрами в основном являются переменные, которые принимают значения аргументов, передаваемых методу при сю вызове. Если метод не имеет параметров, то список параметров будет пустым. Добавление метода к классу Vehicle Мы уже говорили ранее, что методы класса обычно работают с данными и обеспе- чивают доступ к данным класса. Теперь вспомним, что в методе Main() из преды- дущею примера для вычисления расстояния, которое может проехать автомобиль с полным топливным баком, значение расстояния, которое автомобиль может про- ехать, используя один галлон топлива, умножалось на значение емкости топливного бака. Хотя технически мы все выполнили правильно, этот способ нс является оптимальным при выполнении такого вычисления. Максимальную дальность поезд- ки лучше всего вычислять в самом классе vehicle, поскольку максимальное рас- стояние, которое может проехать автомобиль, зависит от емкости его топливного бака и расхода топлива, а обе эти величины инкапсулированы в классе vehicle. Кроме того, при добавлении к классу Vehicle метода, в котором вычисляется дальность поездки, улучшается объектно-ориентированная структура класса. Для того чтобы добавить метод к классу Vehicle, его нужно определить при создании класса. Например, следующая версия класса Vehicle содержит метод range (), выводящий данные о расстоянии, пройденном автомобилем: ‘ В этой программе в классе Vehicle определен метод range. us^ng System; class Vehicle { public int passengers; public int fuelcap; public int mpg; // Количество пассажиров. /! Емкость топливного бака (в галлонах). // Расстояние (в милях), которое данный // автомобиль может проехать, используя один // галлон топлива. // Определение метода range. .................. public void range () { Ч—— ---------J Метод range () принадлежит классу Vehicle. Console. WriteLine (’’Этот автомобиль может проехать с полным ” + ” топливным баком ”+ fuelcap * mpg + ” миль.''); Обратите внимание, *по переменные fuelcap и mpg используются I непосредственно без использования оператора точка (.), class AddMeth {
- классы, оОъекты и метох public static void Main() { Vehicle minivan - new Vehicle(); Vehicle sportscar - new Vehicle(); // Значения присваиваются переменным объекта minivan. minivan.passengers — 7; minivan.fuelcap - 16; minivan.mpg - 21; // Значения присваиваются переменным объекта sportscar. sportscar.passengers -2; sportscar.fuelcap - 14; sportscar. mpg ~ 12; Console.Write("Микроавтобус может перевозить " + minivan.passengers + " пассажиров.\n"); minivan.range(); // Вывод на экран значения максимального расстояния, // которое может проехать микроавтобус. Console .Write (''Спортивный автомобиль может перевозить " -+ sportscar.passengers + ” пассажиров.\п"); sportscar.range(); // Вывод на экран значения максимального расстояния, // которое может проехать спортивный автомобиль. } г Результат работы этой программы будет следующим: " Микроавтобус может перевозить 7 пассажиров. Этот автомобиль может проехать с полным топливным баком 336 миль. Спортивный автомобиль может перевозить 2 пассажиров. Этот автомобиль может проехать с полным топливным баком 168 миль. Теперь рассмотрим все ключевые элементы программы. Начнем с метода ranged. Вот начало объявления метода range (): public void ranged { В этой строке кода объявляется метод с именем range, не имеющий параметров. Он определен как public, что позволяет вызывать его в любой другой части программы, '"'id — тип возвращаемого им значения, то есть метод ranged нс возвращает в программу никакого значения. Строка заканчивается открывающей фигурной скоб- кой!, за которой определяются операторы метода. Тело метода range () содержит единственную строку Console .WriteLine (’’Этот автомобиль может проехать с полным " + " топливным баком "+ fuelcap * mpg - " миль.”); Этот оператор выводит значение максимальной дальности поездки, вычисляя его путем умножения значений переменных fuelcap и mpg. Поскольку каждый объект тина Vehicle имеет собственные копии переменных fuelcap и mpg, при вызове метода range О для вычисления дальности поездки используются копии переменных вызывающего объе кта. Метод ranged заканчивается закрывающей фигурной скобкой. При этом управле- ние передается в точку программы, из которой метод был вызван.
Методы 141 Обратите внимание на оператор minivan . range () ; внутри метода Main (). Этот оператор вызывает метод range () для объекта minivan, после чего управление передается этому методу. То есть вызывается метод range О , который будет работать с переменными объекта minivan. Для этого указывается имя объекта, за которым следует оператор точка (.). Когда метод выполнит свою работу, управление будет передано в точку программы, из которой метод был вызван. Выполнение программы продолжается со строки кода, следующей за вызовом метода. В ртом случае вызов метода minivan, range () выводит информацию о максималь- ном расстоянии, которое может проехать автомобиль minivan, а вызов метода spcrtscar . range () выводит информацию о максимальном расстоянии, которое может проехать автомобиль sportscar. Каждый раз при вызове метод range () работает с переменными объекта, указанного слева от оператора точка (.). Обратите внимание на очень важную особенность определения метода range () : переменные экземпляра fuelcap и mpg указываются непосредственно (для иденти- фикации переменной не используется оператор точка (.) и не указывается имя объекта). Когда метод работает с переменной экземпляра, которая определена в том же классе, что и сам метод, он обращается к переменной без явного указания объекта и без использования оператора точка (то есть в пределах метода нет необходимости указывать имя объекта второй раз). Это означает, что используемые переменные fuelcap и mpg неявно относятся к копиям переменных, которые определены в объекте, вызывающем метод range (). Возврат управления из метода Управление возвращается из метода в двух случаях. Это происходит, во-первых, когда встречается закрывающая фигурная скобка этого метода, во-вторых, когда выполня- ется оператор return. Существует две формы оператора return, одна форма используется в методах, имеющих тип void (которые нс возвращают значения), а вторая — в методах, возвращающих значения. В методах, имеющих тип void, выполнение оператора return приводит к немедлен- ному завершению работы метода. Поэтому обычно этот оператор указывается после условного выражения, поскольку при определенных условиях выполнение оставших- ся операторов метода может не иметь смысла. Оператор return записывается следующим образом: return; Когда он выполняется, управление передается в точку программы, откуда метод был вызван, при этом весь оставшийся в методе код пропускается. В качестве примера рассмотрим следующий метод: Public void myMethO { int i; for(i=0; i<10; i++) { if(i == 5) return; // Метод прекращает свою работу, когда переменная i // принимает значение 5. Console.WriteLine() ;
142 Глава 4. Классы, объекты и методь Здесь цикл for выполнится только 5 раз, потому что, когда значение переменной i станет равным 5, будет выполнен оператор return. В рамках одного метода допускается наличие более одного оператора return. Чаше всего такие операторы используются в iex методах, которые имеют несколько условных выражении. Например. puD1г с vc d у Me th () // ... if(condition_l) return; / / if(condition_2) return; statements; f Возврат управления из этого метода осуществляется в трех случаях: после выполнения заключительных операторов метода, после выполнения первого условного выраже- ния и после выполнения второго условного выражения. Но если метод имеет слишком много точек выхода, это лсструктурирует код, поэтому следует избегать чрезмерного использования операторов return. Возвращение методом значений Хотя методы, имеющие тип void, встречаются достаточно часто, большинство методов в C# возвращают значения. Фактически способность возвращать значение является одним из наиболее важных свойств метода. Вы уже встречали пример возвращения методом значения, когда мы использовали функцию i-tath. Sqrt о для получения значения квадратного корня числа. Возвращение значений в программировании используется для различных целей). В одних случаях, например, при использовании функции r-iat.n. Sqrt О, возвращае- мое значение является результатом некоторого вычисления. В других случаях возвра- щаемое значение может указывать на успех или неудачу операции (возвращается булево значение). Иногда возвращаемое значение может содержать код состояния (возвращается заранее определенное число — -I. О и так далее). Использование возвращаемых методом значений — неотъемлемая часть программирования. Методы возвращают значение вызывающей подпрограмме, используя следующую форму оператора return: return value; Здесь подстановочное слово value — возвращаемое значение. Способность методов возвращать значения можно использовать для усовершенство- вания уже знакомого нам метода range (). Вместо вывода информации о максималь- ном расстоянии, которое может проехать данный автомобиль, новый вариант метода будет вычислять значение максимального расстояния и возвращать его программе. Одним из преимуществ такого подхода является возможность использо- вания возвращаемого значения для других вычислений. В следующем варианте программы модифицированный метод range о нс выводит на экран соответствую- щую информацию сданном автомобиле, а возвращает вычисленное значение. '/ В программе демонстрируется использование метода, // аозвращающех’О вычисленное значение.
Методы 143 ng System; eiss Vehicle { public int passengers; public int fuelcap; public int mpg; // Количество пассажиров. // Емкость топливного бака (в валлонах). // Расстояние (в милях), которое данный // автомобиль может проехать, используя один // галлон топлива. // Определение метода, public int range() i возврацаюшсго знач типа tn return mpg к fuelcap; «4- Теперь mcio;i range () возвращает значение. •^ass RetMeth { public static void Main() { Vehicle minivan = new Vehicle(); Vehicle sportscar — new Vehicle(); int rangel, range2; // Значения присваиваются переменным объекта minivan. minivan.passengers 7; minivan.fuelcap - 16; minivan.mpg - 21; 11 Значения присваиваются переменным объекта sportscar. sportscar .passengers = 2; sportscar.fuelcap = 14; sportscar.mpg - 12; 11 Вычисление максимального расстояния, которое может проехать // данный автомобиль . г----------------------—------ range2 = sportscar, г а п д е () ; L*~------- ---------- Console-WriteLine ("Микроавтобус может перевезти " a minivan.passengers 't- пассажиров на расстояние ’’ + rangel + ’’ миль."); Console.WriteLine ("Спортивный автомобиль может перевезти " + sportscar.passengers + " пассажиров на расстояние ’’ а гапде2 -» ” миль."); i Результат выполнения этой программы следующий: Микроавтобус может перевезти 7 пассажиров на расстояние 336 миль. Спортивный автомобиль может перевезти 2 пассажиров на расстояние 168 миль. В этой программе вызов метода range () помещен справа от оператора присваивания. Слева от него находится переменная, которой будет присвоено значение, возвращае- мое методом range (). Следовательно, после выполнения строки кода rangel . nu.nivam. range () ; переменной rangel присваивается значение, для вычисления которого использова- лись переменные объекта minivan.
144 Глава 4. Классы, объекты и методы Теперь метод range () возвращает значение типа int. Очень важно правильно указать тип возвращаемого методом значения (если возникнет необходимость возврата методом данных типа double, при объявлении метода следует указать именно этот тип возвращаемого значения). Хотя синтаксис представленной выше программы нс содержит ошибок, она написана недостаточно эффективно. В частности, отсутствует необходимость в переменных rangel и range2, поскольку метод range () может быть вызван непосредственно в операторе WriteLine () ;, как это показано ниже. Console.WriteLine("Микроавтобус может перевезти ’’ + minivan .passengers -г ” пассажиров на расстояние ’’ т minivan. range () " миль,"); В этом случае при выполнении оператора WriteLine () ; автоматически вызывается метод minivan. range о , а его значение передается методу WriteLine О- Кроме того, вы можете вызвать метод range () в любом месте программы, где необходимо использование значения максимального расстояния данного объекта класса Vehicle. Например, в следующей строке кода производится сравнение значений, возвращае- мых методом range (). if(vl.range () > v2. range ()) Console.WriteLine("Автомобиль vl с полным топливным баком может проехать’Ч- "\п большее расстояние, чем автомобиль v2.''); пр&фесашша-— ......... ..——...— — Вопрос. Я слышал, что компилятор C# может обнаружить код, который не будет выполнен ни при каких условиях. Что это означает? Ответ. Это действительно так, если компилятор обнаружит в созданном вами методе код, который не может быть выполнен, он выдаст предупреждающее сообще- ние. Рассмотрим пример, public void m() { char a, b; if(a-=b) { Console.WriteLine("Переменные а и b равны.”); return; } else { Console.WriteLine("Переменные а и b равны."); return; ) Console.WriteLine("Эта строка кода никогда не будет выполнена."); } Здесь возврат управления из метода гл () всегда будет происходить до выполнения последнего оператора WriteLine ();. Если вы попытаетесь скомпилировать этот метод, будет выдано сообщение, что в программе обнаружен код, который никогда нс будет выполнен. Появление в программе невыполнимого кода — это результат ошибки программиста, поэтому к таким сообщениям всегда следует относиться серьезно.
Методы 145 Использование параметров При вызове метода ему можно передавать одно или несколько значений. Как уже говорилось, передаваемое методу значение называется аргументом. Переменная BHvrpn метода, которая принимает аргумент, называется параметром. Параметры объявляются внутри круглых скобок, следующих за именем метода. Синтаксис объявления параметров аналогичен синтаксису объявления переменных. Параметр находится в области видимости своего метода. Назначение параметра — при выпол- нении метода принимать значение аргумента, но в целом он действует, как любая другая локальная переменная. Далее приведена простая программа, в которой для передачи методу значения используется параметр. Внутри класса CkhNum метод isEven () возвращает значение time, если передаваемое ему значение является четным. В противном случае он возвращает значение false. То есть метод isEven () возвращает значения типа bool. / / Простая программа, в которой демонстрируется использование параметра для // передачи методу значения. using System; class ChkNum { ,// Определение метода, который возвращает значение типа bool. public bool isEven (int x) { ч----------------------Переменная x является параметром if((x%2) == 0) return true; метода isEven () и имеет тип int. else return false; class ParmDemo { public static void MainQ { ChkNum e - new ChkNum (); if(e.isEven(10)) Console.WriteLine("Число 10 четное."); if(e.isEven(9)) Console.WriteLine("Число 9 четное."); if(e.isEven(8)) Console.WriteLine("Число 8 четное."); В результате выполнения этой программы будут выведены следующие строки: Число 10 четное. Число 8 четное. Метод isEven () вызывается три раза и каждый раз ему передается новое значение. Рассмотрим этот процесс более подробно, при этом обратим внимание на способ вызова метода isEven () — аргумент указывается внутри круглых скобок. Когда метод isEven () вызывается первый раз, ему передается значение 10 (при выполне- нии метода параметр х будет иметь значение 10). Во втором вызове аргументом является значение 9 (параметр х будет иметь значение 9). При третьем вызове параметр х получает значение 8. То есть параметр метода, переменная х, получает значение, которое передается как аргумент во время вызова метода isEven ().
14b Глава 4. Классы, объекты и методы | ------—------- j Метод может иметь несколько параметрон. Объявляемые параметры разделяются запятыми. Например, класс Factor содержит .метод Facte г О , в котором опреде- ляется, является ли первый параметр делителем второго параметра. using System; class Factor { _____ public bool isFactor(int a, int b) { 4 -—~| э гот метод имеет два параметра?] iff (b % а) 0) return true; else return false; } 1 class IsFact ( public static void Main() < Factor x - new Factor (); if(x.isFactor(2, 20)) Console. WriteLine (’’Число 2 является делителем числа 20.”); if(х.isFactor(3, 20)) Console. WriteLine (’’Эта строка не будет выведена на экран."); Обратите внимание, что при вызове метода isFactor () аргументы тоже разделены запятыми. Используемые параметры могут иметь различные типы. Например, такой фрагмент кода int rnyMethfint a, double b, float с) { и ... не является ошибочным. Дальнейшее усовершенствование класса Vehicle Можно продолжить усовершенствование класса Vehicle и добавить возможность вычисления количества топлива, необходимого автомобилю для преодоления задан- ного расстояния. Новый метод, который будет иметь имя fuel needed (), принимает в качестве аргумента значение расстояния, которое необходимо преодолеть автомо- билю, и возвращает значение необходимого количества топлива (в галлонах). При- ведем определение метода fuelneededf) . public double fuelneedea(int miles) { return (double) miles / mpg; i Обратите внимание, что метод возвращает значение типа double. Это весьма уместно, поскольку значение необходимого количества топлива не обязательно будет целочисленным. Далее представлен весь код класса Vehicle, в котором определен метод fuelnceded (}. /‘ Для вычисления количества топлива/ необходимого автомобилю, чтобы преодолеть указанное расстояние, в программе используется метод с параметром. */
Методы 147 ass Vehicle { public inc passengers; nunlie mt fuelcap; public int mpg; // Количество пассажиров. г/ Емкость топливного бака (в галлонах). / Расстояние (в милях) , которое данный // автомобиль может проехать, используя один / галлон топлива . Метод возвращает значение максимального расстояния, которое может // проехать автомобиль с полным топливным баком, public int range () < return mpg * fuelcap; } z Метод вычисляет количество топлива, требуемое для преодоления расстояния, значение которого передается методу в качестве параметра. public double fuelneeded(int miles) i return (double) miles / mpg; crass CompFuel { public static void Main() { Vehicle minivan = new Vehicle (); vehicle sportscar = new Vehicle(); double gallons; int dist - 252; // Значения присваиваются переменным объекта minivan. minivan.passengers — 7; minivan.fuelcap = 16; minivan.mpg = 21; 11 Значения присваиваются переменным объекта sportscar. sportscar.passengers - 2; sportscar.fuelcap = 14; sportscar.mpg - 12; gallons - minivan.fuelneeded(dist); Console. WriteLine ("Чтобы проехать " r dist -г " мили, микроавтобусу " + "требуется ’’ u gallons + " галлонов топлива."); gallons = sportscar.fuelneeded(dist); Console.WriteLine("Чтобы проехать " в dist + " мили, спортивному " + "автомобилю требуется " gallons т " галлон топлива."); Ниже представлен результат выполнения этой программы. Чтобы проехать 252 мили, микроавтобусу требуется 12.0 галлонов топлива. Чтобы проехать 252 мили, спортивному автомобилю требуется 21.0 галлон топлива.
। лава 4. Классы, объекты и методы X Минутный практикум to / I. В каких случаях для доступа к переменной экземпляра или к методу нужно j q\ 3 указывать имя объекта и использовать оператор точка (.)? В каких случаях имя переменной или метода может использоваться непосредственно? 2. Объясните разницу между аргументом и параметром. 3. Назовите два способа возврата управления из метода. Проект 4-1. Создание справочного класса [3 HelpClassDemo.cs Классы необходимы большой программе в качестве строительных блоков. Для этого каждый класс должен представлять собой единый (законченный) функциональный блок, выполняющий четко определенные действия. Классы должны быть небольшими насколько это возможно, но в то же время и не слишком маленькими. Потому что классы, для которых заданы некоторые второстепенные функции, запутывают и деструктурируют код, а классы, которые выполняют недостаточно функций, не предоставляют программе необходимую функциональность. Золотая середина находится там, где наука о програм- мировании переходит в искусство программирования. Поэтому чем больше у програм- миста опыта, тем быстрее он находит оптимальное решение. Попробуем для тренировки преобразовать справочную систему из проекта 3-3 (см. пре- дыдущую главу) в справочный класс. Сначала рассмотрим, чем хороша эта идея. Во-первых, справочная система представляет один логический блок. Поскольку задачей является вывод на экран синтаксиса операторов С#, выполняемая функция блока компактна и четко определена. Во-вторых, помещение справочной системы в класс облегчает работу программиста — если в какой-либо программе понадобится предложить пользователю справочную систему, то достаточно будет создать объект, являющийся экземпляром данного класса. Наконец, поскольку справка инкапсули- рована, ее можно обновить или дополнить, причем это не приведет к побочным эффектам в программе, использующей справочную систему. Пошаговая инструкция 1. Создайте новый файл и назовите его HelpclassDemo.es. Чтобы не вводить код заново, вы можете скопировать код программы из файла Help3. cs в новый файл HelpClassDemo.cs. 1. Для доступа к переменной экземпляра из кода, не являющегося частью класса, в котором определена эта переменная, необходимо указать имя объекта, оператор точка (.) и имя переменной. Для доступа к переменной экземпляра из кода, являющегося частью класса, в котором определена данная переменная, можно непосредственно указать имя переменной. Эти же правила справедливы для методов. 2. Аргумент — это значение, которое передается методу при его вызове. Параметром является определенная методом переменная, которая принимает значение аргумента. 3. Возврат управления из метода может осуществляться с помощью оператора return. Если метод имеет тип void, то возврат управления в то место программы, из которого метод был вызван, осуществляется при достижении закрывающей фигурной скобки. Методы, возвра- щающие значение какого-либо типа (не-void), должны использовать оператор return для возврата значения указанного типа, то есть возврат управления из метода при достижении закрывающей фигурной скобки невозможен.
Методы 149 2 Для создания класса нужно точно определить составляющие справочной системы. Например, в проекте Help3.cs определен код для вывода на экран меню, ввода пользователем запроса, проверки корректности запроса и вывода на экран инфор- мации о выбранном элементе меню. Программа продолжает выполнять цикл до тех пор, пока пользователь не введет символ q. Очевидно, что вывод меню, проверка правильности ввода и вывод информации являются неотъемлемыми частями справочной системы. А получение программой пользовательского ввода п определение ситуаций, когда необходимо обрабатывать повторные запросы пользователя, — это вопросы, которые не являются неотъемлемыми компонента- ми справочной системы. Следовательно, в нашу задачу входит создание класса, выводящего меню, проверяющего корректность запроса и выводящего запраши- ваемую информацию, назовем эти методы showmenu о , isvaliaO и helpon () соответственно. 3. Создайте метод helpon о , как показано ниже: public void helpon(char what) { switch(what) ( case ' 1 ’ : Console.WriteLine("Оператор if:\n”); Console.WriteLine ("if (condition) statement; ’’) ; Console.WriteLine("else statement;"); oreak; case '2’: Console.WriteLine("Оператор switch:\n”); Console.WriteLine("switch(expression) {’’) ; Console.WriteLine(" case constant:"); Console.WriteLine(” statement sequence”); Console.WriteLine(" break;”); Console.WriteLine(" // ..."); Console.WriteLine(")"); break; case ’ 3' : Console.WriteLine("Цикл for:\n”); Console.Write("for(init; condition; iteration)’’); Console. WriteLine (’’ statement; ") ; break; case ' 4 ' : Console.WriteLine ("Цикл while;\n"); Console.WriteLine("while(condition) statement;”); break; case '5 ' : Console.WriteLine ( "Цикл do-while:\n"); Console.WriteLine("do {"); Console.WriteLine(" statement;"); Console.WriteLine("} while (condition);”); break; case ’6': Console.WriteLine("Оператор break:\n"); Console.WriteLine("break; or break label;"); break; case ' 7 ’ : Console.WriteLine("Оператор continue:\n"); Console.WriteLine("continue; or continue label;"); break; case ’8': Console-WriteLine("Оператор goto:\n");
(□и Глава 4. Классы, объекты и методы Console.WriceLine("goto label;") ; break; Console.WriteLine(); } 4. Создайте метод showmenu () следующим образом: public void showmenu () { Console.WriteLine("Справка по синтаксису: "); Console.WriteLine(" Console.WriteLine(" Console.WriteLine (’’ Console.WriteLine (" Console.WriteLine(" Console.WriteLine(” Console.WriteLine(" Console.WriteLine(" 1. Оператор if”); 2. Оператор switch"); 3. Цикл for"); 4. Цикл while"); 5. Цикл do-while"); 6. Оператор break’’); 7. Оператор continue"); 8. Оператор goto\n"); Console.WriteLine("Введите порядковый номер оператора итп Console.WriteLine("Для завершения работы программы введит» i цикла: '*) ; i символ q: "); 5. Создайте метод isvalid О , как показано ниже: public bool isvalid(char ch) { if(ch < i ch > '8' & ch else return true; 'q') return false; 6. Создайте из этих трех методов класс Help: class Help { public void helpon(char what) { switch(what) { case '1 * : Console.WriteLine("Оператор if:\n"); Console.WriteLine("if(condition) statement;"); Console.WriteLine("else statement;"); break; case ’ 2 ’ : Console.WriteLine("Оператор switch:\n"); Console.WriteLine("switch(expression) {"); Console. WriteLine (’’ case constant:"); Console.WriteLine(" statement sequence"); Console.WriteLine (’’ break;”); Console.WriteLine(” // ,..”); Console . WriteLine ("}’’) ; break; case ’3 ’ : Console . WriteLine ("Цикл for:\n”) ; Console.Write("for (init; condition; iteration)”); Console.WriteLine(" statement;"); break; case ' 4 ’ : Console.WriteLine("Цикл while:\n"); Console.WriteLine("while(condition) statement;"); creak; case ’ 5' : Console.WriteLine("Цикл do-while: \n");
151 Методы Console.WriteLine("do {“); Console.WriteLine(” statement;"); Console.WriteLine(”» while (condition);”); break; case ’ 6 ’ : Console.WriteLine("Оператор break:\n"); Console . WriteLine ("break; or break label;’’); break; case ’7’: Console.WriteLine("Оператор continue:\n") ; Console.WiiteLine("continue; or continue хами;"); case '8': Console.WriteLine("Оператор goto:\n”); Console.WriteLine("goto label;"); break; } Console.WriteLine(); puolic void showmenu() { console.WriteLine("Справка no синтаксису: ) Console.WriteLine (” Console.WriteLine (•" Console.WriteLine Console.WriteLine (” Console.WriteLine(" Console.WriteLine (” Console.WriteLine (" Console.WriteLine (" 1. Оператор if’’); 2. Оператор switch"); 3. Цикл for"); 4. Цикл while”); 5. Цикл do-while”); 6. Оператор break"); 7. Оператор continue"); 8. Оператор goto\n"); Console.WriteLine("Введите порядковый номер оператора или цикла: "); Console-WriteLine ("Для завершения работы программы введите символ q: ’’); public bool isvalid(char ch) ( if(ch < '1' | ch > '8' & ch !- else return true; ’q’) return false; Теперь необходимо переписать метод Main () из проекта 3-3 таким образом, чтобы в нем использовался новый класс Help. Назовем главный класс (то есть класс, в котором находится метод Main ()) HelpClassDerno. cs. Полный листинг програм- мы ilelpClassi'emo. cs представлен ниже. Проект 4-1. На основе справочной системы из проекта 3“3 создан класс Help. us^ng System; class Help { public void hcipon(char what) {
switch(what) { case ’1’: Console.WriteLine("Оператор if:\n”); Console.WriteLine ("if(condition) statement;") ; Console.WriteLine("else statement;"); break; case ’ 2 ' : Console.WriteLine("Оператор switch:\n"); Console.WriteLine ("switch (expression) {’’) ; Console. WriteLine (" case constant:’’); Console.WriteLine(" statement sequence"); Console.WriteLine(” break;"); Console. WriteLine (’’ // ...”); Console.WriteLine(")"); break; case ’3’: Console.WriteLine("Цикл for:\n"); Console.Write("for(init; condition; iteration)") Console-WriteLine(" statement;"); break; case ’ 4 ’ ; Console-WriteLine (’’Цикл while :\n") ; Console.WriteLine ("while (condition) statement; *’) break; case ’5’: Console.WriteLine("Цикл do-while;\n”); Console - WriteLine ("do {’’); Console.WriteLine(*’ statement;"); Console.WriteLine("} while (condition);”); break; case ’6': Console -WriteLine("Оператор break:\n”); Console.WriteLine("break; or break label;"); break; case '7': Console.WriteLine("Оператор continue:\n"); Console.WriteLine("continue; or continue label;" break; case ' 8 ' : Console.WriteLine("Оператор goto:\n"); Console. WriteLine (’’goto label; ") ; break; } Console.WriteLine(); public void showmenu() { Console.WriteLine ("Справка no синтаксису: ’’) Console-WriteLine (’’ Console.WriteLine(" Console.WriteLine (’’ Console.WriteLine(” Console.WriteLine(" Console.WriteLine(’’ Console.WriteLine (’’ Console.WriteLine (’’ 1. Оператор if’’); 2. Оператор switch"); 3. Цикл for”); 4. Цикл while"); 5. Цикл do-while"); 6. Оператор break"); 7. Оператор continue"); 8. Оператор goto\n’’);
Конструкторы 153 Console.WriteLine("Введите порядковый номер оператора или цикла: ”); Console.WriteLine("Для завершения работы программы введите символ q: ’’) ; public bool isvalid(char ch) { if(ch < ’1' I ch > *8' & ch 'q') return false; else return true; class HelpClassDerno { public static void Main() { char choice; Help hlpobj - new HelpO; for(;;) { do { hlpobj.showmenu(); do { choice = (char) Console.Read () ; } while(choice == *\n’ | choice == ) while( 1hlpobj.isvalid(choice) ); if(choice -= ’q’) break; Console.WriteLine("\n") ; hlpobj.helpon(choice) ; } ) Тестирование данной программы показывает, что выполняемые ею функции остались прежними. Преимущество же нового подхода состоит в том, что теперь в распоряжении программиста есть справочная система, которая может исполь- зоваться повторно, когда в этом возникнет необходимость. Конструкторы В предыдущих примерах переменным экземпляра каждого объекта класса vehicle необходимо было присваивать значения вручную, используя следующую последова- тельность операторов: -cnivan.passengers = 7; ^tnivan.fuelcap = 16; ni van. mpg = 21; 1акой способ никогда не используется в профессионально написанных программах Cs, потому что, во-первых, при ручном вводе всегда существует вероятность случай- ной ошибки (вы можете забыть присвоить значение одной из переменных), во-вто- Рых, в C# предусмотрен более эффективный способ выполнения этой задачи — использование конструктора.
154 Глава 4. Классы, объекты и методы Конструктор класса инициализирует объект при его создании. Он имеет то же имя, что и его класс, и синтаксически похож на метод. Однако в конструкторах тип возвращаемого значения не указывается явно. Общий синтаксис конструктора: class-name() { код конструктора Как правило, конструкторы используются для присваивания начальных значений переменным экземпляра, определенным классом, или Х1я выполнения любых других процедур инициализации, необходимых для создания полностью сформированного объекта. Все классы имеют конструкторы независимо от того, определен он или нет. По умолчанию в СТ предусмотрено наличие конструктора, который присваивает нулевые значения веем переменным экземпляра (для переменных обычных типов) и значения null (для переменных ссылочного типа). Но если конструктор явно определен в классе, то конструктор по умолчанию использоваться не будет. Рассмотрим простой пример: // Пример простого конструктора. using System; class MyClass { public int x; public MyClass () { x - IC; Конструктор класса My class. class ConsDemo { public static void Main() { MyClass tl - new MyClass(); MyClass t2 - new MyClass(); Console.WriteLine(tl.x + ” " + t2.x); } В этом примере используется конструктор класса MyClass public MyClass () i x - 10; } Отметим, что конструктор определен как public. Это сделано потому, что конструк- тор будет вызываться из кода, определенного за пределами его класса. Конструктор присваивает переменной экземпляра х класса MyClass значение 10. Этот конструктор вызывается оператором new при создании объекта. Например, в строке кода MyClass tl -- new MyClass О; конструктор MyClass О вызывается для создания объекта tl, присваивая перемен- ной tl . х значение 10. То же происходит и для объекта t2 - после вызова конструк- тора переменная 12.x. также получает значение 10. Таким образом, в результате выполнения программы будут выведены следующие значения: 10 10
конструкторы 155 Конструкторы с параметрами 13 предыдущем примере был использован конструктор без параметров. Иногда это приемлемо, но чаше применяется конструктор с одним или несколькими парамет- рами. Параметры используются в конструкторе так же, как в методе. В следующем примере представлен класс MyClass, содержащий конструктор с параметрами. , . ис1.ользова1:ие конструктора с параметрами. us-nq System; class MyClass { puolic int x; public MyClass(int i) { class ParmConsDemo { public static void MainO { MyClass tl - new MyClass (10); MyClass t2 new MyClass(88); Console.WriteLine (tl.x + " ” + t2.x); Ниже показан результат выполнения этой программы. 10 88 В данной версии класса MyClass конструктор MyClass О имеет один параметр (переменную i типа int), который используется для присваивания начального значения переменной экземпляра х. Следовательно, после выполнения оператора MyClass tl = new MyClass(10); значение 10 передается параметру i, а затем оно присваивается переменной х. Добавление конструктора к классу Vehicle Можно усовершенствовать класс Vehicle, добавив к нему конструктор, который при создании объекта будет автоматически инициализировать переменные passengers, f celcap и mpg. Обратите особое внимание на строки кода, в которых создаются объекты класса Vehicle. ' В программе используется новая версия класса Vehicle, в которой определен ' / конструктор. using System; -lass Vehicle { public int passengers; // Количество пассажиров. public inc fuelcap; // Емкость топливного бака (в галлонах). public int mpg; // Расстояние (в милях), которое данный автомобиль // может проехать, используя один галлон топлива. / Конструктор класса Vehicle. . ____ _________ public Vehicle (int p, int f, int m) {<----jКонсгру ктор oaccaVehic 1 e7| passengers = p;
156 Глава 4. Классы, объекты и методы fuelcap =• f; mpg - m; } // Метод возвращает значение максимального расстояния, которое может // проехать автомобиль с полным топливным баком. public inc range() i recurn mpg * fuelcap; } // Метод вычисляет количество топлива, необходимое для преодоления // расстояния, значение которого передается методу в качестве параметра, public double fuelneeded(int miles) { return (double) miles / mpg; class VehConsDemo { public static void Main() { // Создание двух объектов класса Vehicle. Vehicle minivan - new Vehicle(7, 16, 21); Vehicle sportscar - new Vehicle (2, 14, 12); double gallons; int dist = 252; gallons minivan.fuelneeded(dist); Console. WriteLine ("Чтобы проехать " + dist + ’’ мили, микроавтобусу " + "требуется " + gallons + " галлонов топлива."); gallons - sporescar.fuelneeded(dist); Console .WriteLine ("Чтобы проехать " +• dist + ’’ мили, спортивному " - "автомобилю требуется ’’ + gallons + " галлон топлива."); Передача характеристик ав~ томобиля объекту класса Vehicle с использованием его конструктора. ) При создании объектов minivan и sportscar конструктор Vehicle () присваивает переменным этих объектов начальные значения. Каждый объект инициализируется указываемыми аргументами. Например, в строке кода Vehicle minivan - new Vehicle!?, 16, 21); значения 7, 16 и 21 передаются конструктору при создании объекта (выполнении оператора new). Следовательно, копни переменных passengers, fuelcap и mpg объекта minivan будут содержать значения 7, 16 и 21 соответственно, а результат выполнения этой программы будет таким же, как в предыдущей версии. Минутный практикум I. Что такое конструктор? Когда он выполняется? 2. Указывается ли в конструкторе тип возвращаемого значения? 1. Конструктором называется метод, используемый для инициализации создаваемого объекта. Конструктор выполняется при создании объекта его класса. 2. Нет.
Деструкторы и «сборка мусора» ___________________________________________________ фператор new Теперь, когда мы уже достаточно знаем о классах и их конструкторах, подробно рассмотрим оператор new. Этот оператор имеет следующий синтаксис: cjf ass-var — new class-name () ; Здесь словосочетание class-var — создаваемая переменная, тип которой (class) указывается перед именем переменной, а словосочетание class-name — это имя класса, экземпляр которого создается. (Можно сказать, что вместо подстановочных слов class и class-name указывается одно и то же имя класса, экземпляр которого создастся.) Имя класса, за которым следует пара круглых скобок, является конструк- тором класса. Если класс не имеет своего конструктора, оператор new будет исполь- зовать конструктор по умолчанию, предоставленный С#. Следовательно, оператор new может использоваться для создания объекта любого класса. Поскольку размер памяти ограничен, существует вероятность, что оператор new не сможет выделить для объекта нужное количество памяти. Если это произойдет, то возникнет исключительная ситуация выполнения программы. (Как обрабатывать исключительные ситуации, вы узнаете из главы 9.) При выполнении программ этой книги вам не нужно беспокоиться о недостаточном объеме памяти, но в будущем при создании своих программ всегда учитывайте вероятность этого. - — — .............- — — Вопрос. Почему отсутствует необходимость использования оператора new для создания переменных обычного типа (таких, как int или float)? Ответ. В C# переменная обычного типа сама хранит свое значение. Память для хранения этой переменной автоматически выделяется при компилировании программы, и нет необходимости явно выделять память с помощью оператора new. Что касается переменных ссылочного типа, то они хранят только ссылку на объект. Память для хранения объекта выделяется динамически (во время выполнения программы). Превращение переменных обычного типа в переменные ссылочного типа может заметно снизить производительность программы. При использовании переменных ссылочного типа происходят непрямые обращения к значениям, что снижает ско- рость выполнения программы. Для эксперимента можно применить оператор new, чтобы создать переменную обычного типа -nt 1 = new int(); При этом будет вызван конструктор по умолчанию для типа int, который присвоит переменной i начальное значение 0. Однако следует заметить, что при этом память не будет выделена динамически. Большинство программистов нс используют опера- тор new для создания переменных обычного типа. Деструкторы и «сборка мусора» Вы уже знаете, что при использовании оператора new память для объектов выделя- ется динамически. Но поскольку объем памяти ограничен, существует возможность неудачной попытки создания оператором new необходимого объекта. Поэтому один
IDO Глава 4. Классы, объекты и методы из ключевых компонентов схемы динамического выделения памяти механизм/ освобождения памяти, занимаемой неиспользуемыми объектами, для того чтобы ее можно было выделить под вновь создаваемые объск|ы. Во многих языках програм- мирования освобождение ранее выделенной памяти осуществляется вручную (напри- мер, в C++для освобождения памяти используется оператор dele-"). Сч располагает для этого более эффективным механизмом, называемым «сборкой мусора». Когда в Сопрограмме отсутствуют обращения к объекту, то оп рассматривается как не использующийся более, и система автоматически без участия программиста освобождает занимаемую им память, которая в дальнейшем может быть выделена под новые объекты. «Сборка мусора» периодически осуществляется в ходе всей программы, причем для начала этой операции должны выполняться два условия — во-первых, наличие объектов, которые можно удалить из памяти, а во-вторых, необходимость такой операции. Поскольку процесс «сборки мусора» занимает некоторое время, система исполнения программы осуществляет ее только в случае необходимости, причем программист нс может точно определить момент, когда это произойдет. Деструкторы В C# предусмотрена возможность создания метода, который! вызывается непосред- ственно перед удалением объекта из памяти при помощи операции «сборки мусора». Этот метод называется деструктором и может использоваться для гарантии коррект- ного удаления объекта из памяти. Например, с помощью деструктора можно удосто- вериться, что файл, к которому имело место обращение из данного объекта, будет закрыт. Деструктор имеет следующий синтаксис: ~class-name() { // код деструктора ) Здесь словосочетание class-name — это имя класса. То есть деструктор объявляется так же, как конструктор, за исключением того, что в деструкторе перед именем класса используется символ тильда (~). Отмстим, что при определении деструктора не указывается тип возвращаемого значения. Для добавления деструктора к классу нужно просто добавить его определение к коду этого класса (то есть деструктор является обычным членом класса). Внутри деструк- тора назначаются действия, которые должны быть выполнены перед возвращением памяти, выделенной для этого объекта. Деструктор вызывается во всех случаях, когда объект его класса должен быть удален из памяти. Важно понимать, что деструктор вызывается непосредственно перед выполнением операции «сборки мусора». (Он не вызывается, например, когда объект класса выходит из области видимости. Этим деструкторы в С-'; отличаются от деструкторов в C++, где они вызываются, когда объект выходит за пределы области видимости, в которой этот объект был создан.) Это означает, что невозможно точно определить, когда будет выполняться код деструктора. Кроме того, программа может завершить работу до начала операции «сборки .мусора», в этом случае деструктор нс будет вызываться вообще.
Деструкторы и --сборка мусора» 159 (yf □estructDemo.cs Проект 4-2. Демонстрация работы деструкторов ,\1ы уже говорили, что объекты не обязательно удаляются из памяти, если они больше нс используются в программе. «Сборка мусора» производится только тогда, когда она может быть выполнена эффективно (обычно для нескольких объектов одновременно). Следовательно, для демонстрации работы деструктора необходимо создать и уничтожить большое количество объектов. Именно этому мы посвятим данный проект. Пошаговая инструкция I. Создайте новый файл и назовите его Destruct.cs. 2. Создайте класс Destruct, как показано ниже: class Destruct { int '/ Вызывается при удалении объекта из памяти. ~Destruct() { Console . WriteLine (’’Удаление из памяти объекта " г х) ; ) // Метод создает объект, который сразу будет уничтожен, public void generator(int i) { Destruct о = new Destruct(i); Конструктор присваивает переменной экземпляра х значение, передаваемое кон- структору в качестве аргумента. В этой программе деструктор с помощью пере- менной х выводит на экран порядковый номер уничтожаемого объекта. Особое внимание обратите на метод generator о , который создаст и затем сразу унич- тожает объект типа Destruct. В следующем пункте инструкции вы увидите, как это осуществляется. Создайте класс DestructDem.o, как показано ниже: class DestructDemo { public static void Main() { int count; Destruct ob new Destruct(0); /* Далее с помощью цикла for создается большое количество объектов. После использования всей свободной памяти выполняется операция «сборки мусора». Необходимое для этого количество объектов зависит от объема установленной на компьютере памяти, поэтому, зозмож1’.о, придется увеличить число, используемое в условном выражении цикла (количество проходов цикла). */
for(count-1; count < 100000; countrb) ob-generator(count); } } Вначале n этом классе создастся объект nb типа Destruct. Затем с помощью этого объекта и вызова определенного в нем метода generator () создается 100000 объектов. В результате на некотором этапе выполнения программы объекты станут не только создаваться, но и уничтожаться, причем на различных этапах будут происходить операции «сборки мусора». Как часто и когда именно это будет происходить, зависит от нескольких факторов — начального объема свободной памяти, типа операционной системы и так далее. Но с определенного момента на экране начнут появляться сообщения, генерируемые деструктором. Если такие сообщения не выводятся, попробуйте увеличить число создаваемых объектов путем увеличения значения переменной count в цикле for. 4. Ниже представлен полный листинг программы DectructDemo.cs: Проект 4-2. В этой программе демонстрируется работа деструктора. */ using System; class Destruct { public int x; public Destruct(int i) { x - i; } // Вызывается при удалении объекта из памяти. -Destruct() { Console.WriteLine("Destructing " + х) ; ) II Метод создает объект, который сразу уничтожается. public void generator(int i) t Destruct о - new Destruct(i); } } class DestructDeroo { public static void Main() j int count; Destruct ob ~ new Destruct (0) ; /* Далее с помощью цикла for создается большое количество объектов. После использования всей свободной памяти выполняется операция «сборки мусора». Необходимое для этого количество объектов зависит от объема установленной на компьютере памяти, поэтому, возможно, придется увеличить число, используемое в условном выражении цикла (количество проходов цикла). */
^ючееое слово this 161 for(count-1; count < 100000; count++) ob.generator(count); Ключевое слово this В завершение этой главы рассмотрим особенности использования ключевого слова • г. з. При вызове метода ему автоматически передается неявный аргумент, который является ссылкой на вызывающий объект (то есть на объект, с данными которого будет работать метод). Эта ссылка называется this. Чтобы понять, как работает ссылка is. рассмотрим программу, в которой создается класс fwr, предназначенный для вычисления результата возведения числа в некоторую целочисленную степень. using System; class Pwr { public double b; public int e; public double val; public Pwr(double num, int exp) { о - num; e - exp; val 1; uf(exp=-0) return: for( ; exp>0; exp--) val - val ж b; public double get_pwr() return val; Cxass DemoPwr { Public static Pwr x - new Pwr у = new Pwr z = new void Main() { Pwr(4.0, 2); Pwr(2.5, 1); Pwr(5.7, 0); Console.WriteLine(x.b + Console.WriteLine(у.b + Console.WriteLine(z.b + в " -I- x. e -i- ”-й степени a " + у.е + ”-й степени a ’* + z.c + п-й степени + x.gct_pwr()) ; + у.getpwr()); K z.got_pwr()); Kjk вы знаете, в пределах метода доступ к другим членам класса может осуществ- иться без указания имени объекта или класса, то есть непосредственно. Сл слова- ельно. внутри метода get_pwr при выполнении оператора ttTurn val; будет возвращена копия переменной val, ассоциированная с вызывающим объектом. Но этот же оператор можно написать следующим образом: urn this.val;
Глава 4. Классы, объекты и метод Здесь ссылка this указывает на объект, для которого был вызван метод get_pwr (; Следовательно, переменная this.val является копией переменной val данног объекта. Например, если метол get pwr () вызван для объекта х, то ссылка this предыдущем операторе указывает на объект х. Можно сказать, что запись оператор без использования ключевого слова this является сокращенной формой записи. Ниже представлен весь класс Pwr, написанный с использованием ключевого слое this. using System; class Pwr { » public double b; public int e; public double val; public Pwr(double num, int exp) { this.b --- num; this.e - exp; this.val ~ 1; if(exp=—0) return; for( ; exp>0; exp—) this.val this.val * this.b; } public double get_pwr() { return this.val; } } Скорее всего, ни один опытный программист на языке C# нс напишет класс Pwr подобным способом, поскольку никакого преимущества такая форма записи не дает, Но в некоторых случаях ссылка this может быть полезна. Например, синтаксис C# позволяет использовать одинаковые имена для параметров или локальных перемен- ных и для переменных экземпляра. Когда это происходит, локальное имя скрывает переменную экземпляра, и доступ к ней можно получить, используя ссылку this. Например, приведенный ниже конструктор Pwr о является синтаксически действи- тельным, хотя такой стиль применять не рекомендуется. public Pwr(double b, int e) { this.b - b; this.e - e; val - 1; if(e—0) return; for( ; e>0; e—) val - val * b; i В этой версии конструктора Pwr () имена параметров и имена переменных экземп- ляра одинаковы, поэтому переменные экземпляра будут скрыты. А если вы исполь- зуете ссылку this, то можете «открыть» переменную экземпляра.
Контрольные вопросы 163 Контрольные вопросы 1. В чем состоит различие между классом и объектом? 2. Как определяется класс? 3. Собственные копии чего имеет каждый объект? 4. Покажите, как объявить объект с именем counter класса MyCounter, используя два отдельных оператора. 5. Запишите определение метода myMeth (), который возвращает значение типа double и имеет два параметра а и Ь типа тпс. 6. Как из метода возвращается управление, сели метод возвращает значе- ние? 7. Какое имя имеет конструктор? 8. Какие функции выполняет оператор new? 9. Что такое «сборка мусора»? Как эта операция выполняется? Что такое деструктор? 10. Объясните предназначение ключевого слова this?
глава Подробнее о типах j данных и оператора! j J .. 1 ---------Г""----'—........ ) □ Массивы | □ Объекты типа string 1 О Цикл foreach ! □ Побитовые операторы □ Оператор ? ]
массивы 165 g этой главе мы вернемся к описанию типов данных и операторов С#, рассмотрим массивы, данные типа string, побитовые операторы и тернарный оператор ?, а также п0Дробно расскажем о цикле foreacn. Массивы Массив — это коллекция переменных одного типа. При обращении к массиву укатывается его имя и индекс элемента, которому было присвоено значение данного типа. Массивы могут быть как одномерными, так и многомерными, хотя чаще используются одномерные массивы. Массивы применяются для решения многих задач, поскольку предоставляют удобные средства группировки переменных одного типа. Например, вы можете использовать массив для хранения информации о среднесуточной температуре каждого дня месяца или для хранения комплекта сим- волов либо коллекции объектов с данными о ваших однокурсниках, а также для других подобных целей. Данные в массиве организованы так, что ими легко манипулировать, именно в этом заключается главное достоинство массива. Например, если массив хранит информа- цию об оценках, полученных абитуриентом, то путем обработки элементов массива в цикле легко вычислить средний балл экзаменуемого. Кроме того, эти данные легко сортируются. В СТ можно использовать массивы точно так же, как в других языках программиро- вания, но здесь у них есть одна особенность. Она состоит в том, что массивы в C# реализованы как объекты, поэтому в данной книге сначала рассматриваются объекты, азатем массивы. Благодаря такой реализации массивов у С#-программ появляются дополнительные возможности, например, удаление из памяти неиспользуемых мас- сивов при «сборке мусора». Одномерные массивы Одномерный массив — это список однотипных переменных. В программировании такие списки применяются достаточно часто. Например, одномерный массив можно использовать для хранения номеров счетов активных пользователей сети или для хранения текущего рейтинга баскетбольной команды. Для объявления одномерного массива применяется следующий синтаксис: Туре:j array-name = new typelsize]; Здесь слово type указывает тип массива, то есть тип данных каждого элемента, из которых и состоит массив. Обратите внимание на квадратные скобки, которые с-|сдуют за ключевым словом, указывающим тип элементов массива. С помощью этих скобок объявляется одномерный массив (далее в этой главе будет показано, как объявляется двухмерный массив). Количество элементов, которые могут быть поме- щены в массив, определяется величиной site. Поскольку массивы реализованы как Ооьскты, процесс создания массива состоит из двух этапов. На первом этапе объяв- ляется имя массива— имя переменной ссылочного типа. На втором этапе для массива Выделяется память и переменной массива присваивается ссылка на эту область Памяти. То есть в C# с помощью оператора new память массиву выделяется дина- мически.
166 Глава 5. Подробнее о типах данных и операто) Примечание j Если у вас есть опыт программирования на С или C++, то обратите особое вниманий на способ объявления массива в С#. В частности на то, что в C# квадратные скоб^З следуют за ключевым словом, указывающим тип, а не за именем массива. Следующая строка кода создает массив типа int, состоящий из десяти элементов, ссылку на него присваивает переменной ссылочного типа sample: int[] sample = new int[10]; Массив объявляется так же, как объект. Переменная sample хранит ссылку на адрес памяти, выделенной оператором new. Размер выделенной памяти достаточен д_ъ| хранения десяти элементов типа int. Объявление массива (так же как объявление объекта) можно выполнить с помощыи; двух операторов. Например, int[] sample; .J sample - new int [10]; В этом случае после создания переменная массива sample не будет ссылаться н$ какой-либо физический объект. Только после выполнения второго оператора пере-, менной присваивается ссылка на массив. Доступ к отдельному элементу массива осуществляется с использованием индекса1. С его помощью указывается позиция элемента в пределах массива. Первый элемент всех массивов в Cfr имеет нулевой индекс. Поскольку массив sample состоит из 10 элементов, значения индексов элементов этого массива находятся в диапазоне от О до 9. Для индексации массива необходимо указать количество его элементов квадратных скобках. Таким образом, первый элемент массива sample указывается как sample [0], а последний элемент — sample [9]. Например, в приведенной нижо программе в массив sample помещаются значения от 0 до 9. //В программе демонстрируется использование одномерного массива. using System; class ArrayDemo { public static void Main]) { inti] sample -= new int[10); int i; for(i - 0; i < 10; i - i+l)-4-----------1 Первый элемент массива имеет индекс 0. | sampled] - i; ————— for(i - 0; i < 10; i = i + 1)^------------------------ Console.WriteLine ("Элементу sample]" + i + "] присвоено значение: ’’ + sample[i]); В результате работы программы будут выведены следующие десять строк: Элементу Элементу Элементу Элементу Элементу sample[0] sample[1] sample[2] sample[3] sample[4] было присвоено значение: 0 было присвоено значение: 1 было присвоено значение: 2 было присвоено значение: 3 было присвоено значение: 4
Массивы 167 Элеменч.у ддеми н ’1У н x У Элементу Элементу sample[5] sample[6] sample[7] sample[8] sample[9] было присвоено значение: 5 было присвоено значение: 6 было присвоено значение: 7 было присвоено значение: 8 было присвоено значение: 9 Схематически массив sample выглядит следующим образом: 0 1 2 3 4 5 6 7 9 Массивы широко используются в программировании, поскольку дают возможность легко управлять большим количеством однотипных переменных. Например, приве- денная ниже программа с помощью обработки элементов массива nuns в цикле for находит максимальное и минимальное значения элементов этого массива. // Прох'рамма находит минимальное и максимальное значения элементов массива. using System; class MinMax { puolic static void Main() { int[] nums - new int[10]; int min, max; nums[0] nums[1] nums[2] nums[3] nums[4] nums[5] nums[6] nums[7] nums[8] nums[9] - 99; - -10; = 100123; - 18; - - -978; - 5623; == 463; - -9; - 287; - 4 9; min = max - nums[ 0 ] ; for(int i —1; i < 10; i++) { if(nums[i] < min) min = nums[i]; if(nums[i] > max) max - numsfi]; } Console. WriteLine (’’Минимальное значение: " + min 4 ”, максимальное "4 ’’значение: ” 4- max) ; Результат выполнения этой программы будет следующим: р. минимальное значение: -978, максимальное значение: 100123 Инициализация массива Р предыдущей программе значения элементам массива nums задавались вручную с использованием десяти отдельных операторов присваивания. Существует более эф- фективный способ выполнения этой операции. Инициализировать массив можно
168 Глава 5. Подробнее о типах данных и оператору сразу при его создании. Общий синтаксис инициализации одномерного массив^ выглядит так: type[] array-name — (vail, va!2, val3, ... , valN}; Здесь начальные значения заданы величинами от vail до v.?.111. Элементам массива значения присваиваются по очереди слева направо, начиная с элемента с индексом О? В C# массиву автоматически выделяется объем памяти, достаточный для хранения: указываемого количества значений, при этом нет необходимости явно использовать' оператор new. Ниже показан более эффективный способ написания программы* MinMax: Ж // В программе используется инициализация массива при его создании. ж using System; class MinMax { public static void Main() ( int[] nums =- { 99, -10, 100123, 18, -978,^- 5623, 463, -9, 287, 49 }; int min, max; Значения, присваиваемые элементам массива при его создании. min = max nums[0]; т forfint i^l; г < 10; i-t--*-) { if(nums[i] < min) min - nums[i]; if(nums[i] > max) max - numsii]; Console. WriteLine (’’Минимальное значение: " + min + ", максимальное " + "значение: " f max); } } Хотя в этом нет необходимости, вы можете в экспериментальных целях использовать"' для инициализации массива оператор new. Например, способ инициализации мас- сива nums, показанный ниже, не противоречит правилам, но использование опера- 1 тора new в данном случае вовсе не обязательно. int[] nums = new int[] { 99, -10, 100123, 18, -978, 5623, 463, -9, 287, 49 ]; I Такой способ инициализации можно использовать для присваивания ссылки на J новый массив уже существующей переменной ссылочного типа: I int[] nums; Ж nums - new int[l { 99, -10, 100123, 18, -978, 1 5623, 463, -9, 287, 49 }; f В этом случае переменная массива nums объявляется в первом операторе, а инициа- | лизируется во втором. ® .1 Жесткий контроль над граничными значениями индексов I В C# строго контролируется обращение к элементу массива. Следует указывать ® индекс, нс превышающий граничные значения индексов этого массива, в противном ’ случае возникнет ошибка выполнения программы. Чтобы убедиться в этом, протес- I тируйте следующую программу, в которой специально указывается несуществующий Я индекс (то есть производится попытка обращения к несуществующему элементу). I
169 массивы I : ь программе производится попытка обращения к несуществующему элементу массива. . '.a System; ArrayErr { pjblic static void MainO { int.'j sample new int[10]; Согласие' условному выражению цикла for переменная i может принять значение, превышающее верхнее граничное значение индексов массива sample. 1 о г {г - С ; sample[i] Превышено верхнее фаничное значение индексов массива sample. На определенном этапе выполнения программы переменной i присваивается значе- цис 10. При попытке обращения к элементу с индексом 10 программа будет закрыта и будет сгенерирован объект-исключение IndexOutOfRangeExcepticn (подробнее об пом см. в главе 10). g) Минутный практикум i '% 1. С использованием чего осуществляется доступ к элементу массива? ;д 2. Как объявить массив типа char, в котором содержится 10 элементов? 3. Справедливо ли утверждение, что во время выполнения Сопрограммы нс осуществляется контроль над граничными значениями индексов массива? Проект 5-1. Сортировка массива 0 Bubble.cs Поскольку в одномерном массиве данные расположены в индексируемом линейном списке, они легко поддаются сортировке. Вероятно, вы знаете, что существует множество различных алгоритмов сортировки. Среди них можно выделить алгоритм быщрой сортировки, пузырьковый алгоритм и алгоритм сортировки методом Шелла. Наиболее известным, простым и понятным является пузырьковый алгоритм. Хотя он нс очень эффективен (фактически неприемлем для сортировки больших массивов), н<> может с успехом применяться для сортировки небольших массивов. Пошаговая инструкция ' Создайте файл и назовите его Bucblc.cs. Пузырьковый алгоритм сортировки получил имя в соответствии с принципом своей работы. Рассмотрим его подробно. Предположим, имеется массив, состоящий из 20 * Доступ к элементу массива осуществляется с использованием индекса. — Массив типа char, содержащий 10 элементов, объявляется следующим образом: chart] а - new char[lC];. ’ Нет. В C# во время выполнения программы строго запрещено обращаться к несуществую- щим элементам массива.
170 Глава 5. Подробнее о типах данных и операторах элементов, которые нужно отсортировать в порядке возрастания. Сравниваются значения двух находящихся рядом элементов. Если значение второго элемента окажется меньше значения первого, то некоторой промежуточной переменной присваивается значение второго элемента, второму элементу присваивается зна- чение первого элемента, а первому элементу присваивается значение промежу- точной переменной. Можно сказать, что элементы меняются местами, причем элемент с наименьшим значением перемещается к левой границе массива. Это можно сравнить с перемещением пузырька воздуха в воде — из глубины к поверхности, поэтому алгоритм сортировки и получил такое название. Для того чтобы в массиве из 20 элементов сравнить все соседние элементы (первый со вторым, второй с третьим и так далее) необходимо выполнить 20 — I = 19 сравнений, а поскольку наименьшее значение может оказаться у последнего элемента (двадцатого), то для его перемещения в самое начало массива нужно выполнить 19 перемещений. То есть нужно организовать два цикла — внешний и внутренний, каждый из которых должен выполняться 19 раз. Ниже приведен фрагмент кода, составляющий ядро пузырькового алгоритма сортировки. Массив, в котором сортируются элементы, называется nums. // Это код пузырькового алгоритма сортировки. for(a-l; а < size; а++) for (b=size-l; b >= a; b--) { if(nums[b-l] > nums[b]) { // Если значение предыдущего элемента больше // значения последующего элемента, то они t - nums[b-l]; // "меняются местами”. nums[b-l] nums[Ь]; nums[b] - t; } } Чтобы вы хорошо усвоили материал, изложенный в этом абзаце, еще раз коротко повторим принцип работы пузырькового алгоритма. Алгоритм построен на двух циклах for. Значения стоящих рядом элементов массива проверяются во внут- реннем цикле. Когда обнаруживается пара элементов, значения которых распо- ложены не в порядке возрастания, они меняются местами. При каждом проходе элементы с наименьшим значением передвигаются в положение, соответствующее их значению. Внешний цикл повторяет этот процесс до тех пор, пока все элементы массива не будут отсортированы. 3. Далее приведен код всей программы Bubble. /* Проект 5-1. В программе демонстрируется использование пузырькового алгоритма сортировки элементов массива. */ using System; class Bubble { public static void Main() { int[] nums - { 99, -10, 100123, 18, -978, 5623, 463, -9, 287, 49 }; int a, o, t; int size; size 10; // Количество элементов массива.
Многомерные массивы 171 // Отображение значений элементов первоначального массива. Console.Write("Значения элементов первоначального массива: \п”); for(int i=0; i < size; i++) Console.Write(” ” + numsli]); Console.WriteLine(); // Это код пузырькового алгоритма сортировки- for(a-l; а < size; а--»-) for (b^size-1; b >-^ a; b--) { if(nums[b-l] > nums[b]) { // Если значение предыдущего элемента // больше значения последующего элемента, t - nums[b-1]; // то они "меняются местами". nums[b-l] - numslb]; nums[b] = t; } } // Отображение значений элементов отсортированного массива. Console.Write ("Значения элементов отсортированного массива: \п"); for (int i-0; i < size; i-r + ) Console.Write(" " + nums[i]); Console.WriteLine(); } Ниже показан результат выполнения этой программы. Значения элементов первоначального массива: 99, -10, 100123, 18, -978, 5623, 463, -9, 287, 49 Значения элементов отсортированного массива: -978, -10, -9, 18, 49, 99, 287, 463, 5623, 100123 Хотя сортировка данных в соответствии с пузырьковым алгоритмом вполне приемлема для небольших массивов, она становится неэффективной при работе с большими массивами. Наилучшим универсальным алгоритмом сортировки является алгоритм быстрой сортировки. Но он основан на тех свойствах языка С#, которые вам еще не знакомы, об этом алгоритме мы расскажем в главе 6. Многомерные массивы Одномерные массивы являются самыми распространенными в программировании, по и многомерные массивы встречаются не так уж редко. Многомерным называется массив, который имеет две или больше размерностей, а доступ к отдельному элементу осуществляется в нем с помощью указания двух и более индексов. Двухмерные массивы ^амым простым из многомерных массивов является двухмерный массив. В нем Расположение конкретного элемента определяется двумя индексами. Двухмерный 'пассив можно рассматривать как информационную таблицу, один индекс которой указывает номер строки, а другой — номер столбца.
Двухмерный целочисленный массив table с размерностью 10x20 объявляется сле- дующим образом: int],] table new int[10, 20]; Заметьте, что при объявлении данного массива две указываемые размерности массива разделены запятой. В первой части оператора с помощью запятой в квадратны^ скобках [, I указывается, что создается переменная ссылочного типа для двухмерного массива. Выделение памяти для массива с помощью оператора new происходит при выполнении второй части объявления массива: new inc[10, 20] 1 При этом создается массив с размерностью 10x20. Обратите внимание, что значения] размерности разделены запятой. i Для получения доступа к элементу двухмерного массива необходимо указать два! индекса, разделив их запятой. Например, для присваивания элементу массива ta-' Ые[3, 5] значения 10 необходимо использовать следующий оператор: 1аЬ1е[3, 5] = 10; ] Ниже представлена программа, в которой элементам двухмерного массива присваи-. ваются значения от 1 до 12, а затем эти значения выводятся на экран. // В программе демонстрируется использование двухмерного массива. • using System; ? class TwoD { public static void Main() { int t, i; int[,] table new int[3, 4 ] ; -< Объявление двухмерного массива с размерностью 3*4. for(t-0; t < 3; ^t) { for(i^0; i < 4; ++i) { tableft,x] « (t*4)-i-i + l; Console.Write(table[t,i] + " } Console.WriteLine(); В этой программе элемент table [ 0, 0 ] будет иметь значение 1, элемент table [ 0, 1 ] — значение 2, элемент table [0, 2] — значение 3 и т. д. Элемент table [2, 3] будет иметь значение 12. Ниже приведено схематическое изображение массива. Рис. 5.1. Схематическое представление массива table
f(pU/A^4ttHue.. ,..л, gCJ11i у вас есть опыт программирования на С, C++ или Java, обратите внимание на т0 что объявление или доступ к элементам многомерных массивов в С» осуществ- ляется нс так, как в этих языках. В них каждая размерность массива и индексы указываются в отдельных квадратных скобках, а в C# указываемые размерности раздел я юте я заняты м и. Массивы, имеющие более двух размерностей В С- разрешается использование массивов, имеющих более двух размерностей. Ниже представлен общий синтаксис объявления такого многомерного массива: type'., ---/I name -- new typelsizel, size2, . . . ,sizeN] ; Например, при выполнении следующего оператора будет создан целочисленный трехмерный массив с размерностью 4x10x3: int:,,] rcultidim = new ;o, 3]; Чзооы присвоить элементу массива multidim [2, 4, 1) значение 100, необходимо выполнить следующий оператор: SiultidiatZ, 4, 1] _ :ос. Инициализация многомерных массивов Инициализировать многомерный массив можно, поместив список значений каждой размерности в отдельный блок, заключенный в фигурные скобки. Например, ниже показан синтаксис инициализации двухмерного массива. type',' а г га уплате - ? {val, val, val,., val} 'val, val, val,,. {val, val, val,.,. val}, Здесь слово val обозначает присваиваемое значение. Каждый внутренний блок присваивает значения элементам соответствующей строки. В пределах строки первое указываемое значение будет храниться в нулевом элементе строки, второе значение — в первом элементе и так далее. Обратите внимание, что блоки, содержащие значения, разделены запятыми, а за закрывающей фигурной скобкой блока со значениями первой размерности следует точка с запятой. В приведенной ниже программе инициализируется массив sqrs, каждому элементу которого присваивается значение из диапазона 1 — 10 и квадрат этого значения. В программе демонстрируется инициализация двухмерного массива. Jsing System; class Squares ( public static void int!,] sqrs - ( • 1, 1 i , _ I 2, 4 ), Main () Обратите внимание, что каждая строка имеет свой блок значений. {
. । шщлмяе о типах данных и операторах ( 3, 9 i, ( 4, 16 }, ( 5, 25 }, ( 6, 36 }, ( 7, 49 }, { 8, 64 }, { 9, 81 }, { 10, 100 ) int i, j ; for(i-G; i < 10; i + + ) { for(j-0; j < 2; j++) Console.Write(sqrs[i,j] + " "); Console.WriteLine (); Ниже показан результат выполнения этой программы. 1 1 2 4 3 9 4 16 5 25 6 36 7 49 8 64 9 81 10 100 Невыровненные массивы В предыдущей программе демонстрировалось создание двухмерного массива, кото- рый иначе можно назвать прямоугольным массивом. В табличном представлении прямоугольный массив — это двухмерный массив, у которого длина строк одинакова. Кроме этого, в C# можно создавать специальный тип двухмерного массива, который называется невыровненным массивом. Невыровненный массив — это внешний массив, состоящий из внутренних массивов разной длины. Следовательно, невыровненный массив можно использовать для создания таблицы, содержащей строки различной длины. При объявлении невыровненных массивов для указания каждой размерности исполь- зуется отдельная пара квадратных скобок. Например, двухмерный невыровненный массив объявляется следующим образом: type[][) array-name = new typelsize][]; Здесь слово size обозначает количество строк в массиве. Размерность строк нс указывается, и память для них не выделяется. Для каждой строки необходимо выполнение отдельного оператора new. Такая технология позволяет определять строки различной длины. Например, в следующем фрагменте кода при объявлении массива jagged память выделяется только для внешнего массива, а потом при
Невыровненные массивы выполнении трех операторов память выделяется отдельно для каждого внутреннего массива: irlr[)[] jagged - new int[3] [] ; nanged[0] - new int[2]; Tagged[i] - new int[3]; jagged[2] new int[4J; После выполнения последовательности этих операторов невыровненный массив НЫ1ЛЯДИТ следующим образом: jagged [0][0] jagged [1][0] | jagged [O][TJ jagged [1 ][1J jagged [1J[2] jagged [2][0] jagged [2J[1 ] jagged [2J[2] jagged [2][3]~| Рассмотрев эту схему, вы поймете, почему невыровненный массив получил такое название. Когда невыровненный массив создан, доступ к его элементам можно получить, указав каждый индекс в паре квадратных скобок. Например, чтобы присвоить значение I0 элементу массива jagged [2] [1], необходимо использовать следующий оператор: lagged[2][1] = 10; Вспомните, что для доступа к элементам прямоугольного массива применяется иная форма записи. Ниже представлена программа, в которой используется невыровненный! двухмерный массив. В массиве сохраняются данные о количестве пассажиров, пользующихся услугами маршрутного такси, чтобы добраться до аэропорта. Поскольку маршрутное такси совершает неодинаковое количество рейсов в различные дни недели (десять рейсов в рабочие дни и два по субботам и воскресеньям), для хранения данных можно использовать невыровненный массив riders. Заметьте, что вторая размерность для первых пяти строк имеет значение 10, а для оставшихся двух строк — 2. // В программе демонстрируется использование невыровненного массива, using System; - lass Ragged { public static void Main() { int[)[] riders new int[7)[]; riders[0] = new riders [1] = new riders[2] - new riders [3] =--- new riders[4] - new Здесь внутренние массивы состоят из 10 элементов. Здесь внутренние массивы состоят из 2 элемен тов. riders[5] new riders[6] = new int[10]; // Элементам массива присваиваются for(i*-0; i < 5; i + + ) for(j=0; j < 10; j + + ) riders[i][j] =- i + j + 10; некоторые произвольные значения.
лава э. 1 юдроонее о типах данных и операторах for(1-5; i < 7; i++) for(3~C; J < 2; -j+ • ) riders[i][j] =- i + j + 10; Console.WriteLine("Количество пассажиров, перевезенных за один рейс"+ "\пв будние дни: "); for(i=0; i < 5; i++) { for(j=C; j < 10; j+т) Console.Write(riders[i][j] + " ”); Console.WriteLine(); ) Console.WriteLine(); Console.WriteLine("Количество пассажиров., перевезенных за один рейс" + "\пв выходные дни; "); for (1-5; 1 < 7; i + + ) ( for(j-0; з < 2; j+r) Console.Write (riders [i] [] ] -t " ") ; Console.WriteLine(); } } } Невыровненные массивы редко используются в приложениях, но в некоторых ситуациях их применение может оказаться весьма эффективным. Например, если вам требуется очень большой двухмерный массив, который будет заполнен не полностью (то есть в нем используются не все элементы), то удобно использовать именно невыровненный массив. Минутный практикум 1. Как указывается каждая размерность для прямоугольных многомерных А ’11 массивов? 2. Может ли отличаться длина каждого внутреннего массива в невыровненном массиве? 3. Как инициализируются многомерные массивы? Присваивание ссылок на массив Когда значение одной переменной ссылочного типа, которая ссылается на массив, присваивается другой переменной такого типа, то второй переменной присваивается ссылка на тот же массив, на который ссылалась первая переменная. При этом не создается копия массива и не копируется содержимое одного массива в другой. В качестве примера рассмотрим следующую программу: // Присваивание значения одной переменной ссылочного типа, которая ссылается // на массив, другой переменной такого же типа. using System; 1. Размерности, разделенные запятыми, указываются в одних квадратных скобках. 2. Да. 3. Для инициализации многомерного массива список значений каждой размерности помеша- ется в отдельный блок, заключенный в фигурные скобки. Вся эта конструкция указывается после оператора присваивания.
Использование свойства Length 177 class AssignARef { public static void Main() { int i; int[) numsl = new int[10j; int'] nurr.s2 -=- new int[10]; for (i-O; i < 10; i+-r) numsl-ij -= i; for(i-0; i < 10; i++) nums2[i’ - ~i; Console.Write ("Значения элементов массива numsl: *'); for(i=0; i < 10; i++) Console.Write(numsl[i] + " Console.WriteLine(} ; Console.Write("Значения элементов массива nums2: for(1^0; i < 10; 1+*) Console.Write(nums2[1] + " ; Console.WriteLine(}; nums2 = numsl; // После выполнения этого оператора переменная nums2 // ссылается на тот же массив, что и переменная numsl. Console.Write("Значения массива, на который ссылается переменная nums2, ’’+ "\ппосле присваивания ей ссылки: ") ; for(i=0; i < 10; i++) Console.Write(nums2[i] + ” ”); Console.WriteLine (); // Теперь можно изменять значения массива, на который ссылается // переменная numsl, используя переменную nums2. nums2[3] = 99; Console.Write("Значения элементов массива numsl после изменения," + " выполненного с помощью \ппеременной nums2: "); for(i-0; i < 10; i++) Console.Write(numsl[i] t " Console.WriteLine (); Результат выполнения этой программы следующий: Значения элементов массива numsl: 0123456789 Значения элементов массива nums2: 0 -1 -2 -3 ~4 -5 -6 -7 “8 -9 Значения массива, на который ссылается переменная nums2 после присвоения ей ссылки: 0123456789 Значения элементов массива numsl после изменения, выполненного с помощью переменной nums2: 012 99 456789 Как видите, после присваивания значения переменной numsl переменной nums2 обе переменные массива ссылаются на один и тот же объект. Использование свойства Length В C# массивы реализованы как объекты, и это дает определенные преимущества при работе с массивами. Одно из них — возможность использования свойства Length.
178 Глава 5. Подробнее о типах данных и операторах / Каждый массив имеет ассоциированное с ним свойство Length, в котором содер- жится информация о максимальном количестве элементов массива (то есть в массиве (имеется в виду класс) определено поле, содержащее информацию о длине массива). Ниже представлена программа, в которой используется свойство Length. /'/ В программе демонстрируется использование свойства Length. ’ using System; 'j class LengthDemo { public static void Main() { int[] list - new int[10]; ; int[] nums = { 1, 2, 3 }; int[][] table = new int[3][J; // Невыровненный массив table. // Выделение памяти для внутренних массивов. table[0] = new int[] {1, 2, 3); table[1] - new int[] (4, 5}; table[2] = new int[] [6, 1, 8, 9}; Console.WriteLine("Длина массива list - ” + list.Length); Console.WriteLine ("Длина массива nums = ’’ + nums.Length); Console.WriteLine("Длина массива table = ** + table.Length); Console.WriteLine("Длина массива tabiefO] = " + table[0].Length); Console.WriteLine("Длина массива table[l] = " + table[1].Length); Console.WriteLine("Длина массива table[2] = " + table[2].Length); Console.WriteLine(); // Использование свойства Length для инициализации массива list. for(int i=0; i < list.Length; i+-r) list[i] - i * i; J Console.Write("Значения элементов массива list: "); ( // Использование свойства Length для отображения значений элементов // массива list. for(int i=0; i < list.Length; i++) Console.Write (list [i] t- " "); Console.WriteLine(} ; ) } Ниже приведен результат выполнения этой программы. Длина массива list = 10 Длина массива nums = 3 Длина массива table - 3 Длина массива table[0] = 3 Длина массива table[1] - 2 Длина массива table[2] = 4 Значения элементов массива list: 014 9 16 25 36 49 64 81 Особое внимание следует обратить на использование свойства Length в двухмерном £ невыровненном массиве table. Мы уже говорили, что двухмерный невыровненный | массив — это массив массивов. Следовательно, при использовании выражения | table.Length I
Использование свойства Length 179 мы получим количество массивов, которые составляют массив table (в данном случае это количество равно 3). Для получения значения длины каждого внутреннего массива во внешнем массиве table необходимо использовать следующее выражение: table[0].Length В данном случае мы получим значение длины внутреннего массива, имеющего индекс 0. Обратите внимание на то, что в классе LengthDemo в условном выражении цикла г используется значение свойства li st. Length. Поскольку каждый массивхранит информацию о своей длине, ее можно использовать в выражениях. Это позволяет создавать код, который может работать с массивами различной длины. Значение свойства Length не имеет ничего общего с количеством элементов, фактически хранящихся в массиве. Поле Length содержит информацию о числе элементов, которые может хранить массив. Использование свойства Length упрощает работу многих алгоритмов, делая более эффективными некоторые операции с массивами. Например, в следующей програм- ме при копировании значений одного массива в другой свойство Length используется для предотвращения возможности выхода за граничные значения индексов массива (что может привести к исключительной ситуации во время выполнения программы): // В программе демонстрируется использование свойства Length для копирования // одного массива в другой. uoing System; class АСору { public static inc i; int[] numsl int[) nums2 void Main () { new int [10] ; = new int[10]; for(i=0; i < numsl.Length; i++) numsl[i] = i; // Копирование массива numsl в массив nums2. if(nums2.Length >= numsl.Length) for(i =0; i < nums2.Length; i++) nums2[i] ~ numsl[i); for(i-0; i < nums2.Length; Console.Write(nums2[i] + " ”); Здесь свойство Length является ключевой составляющей выполнения двух функций. Во-первых, значение свойства Length используется для выполнения проверки того, позволяет ли размер массива nums2 скопировать в него все элементы массива numsl. Во-вторых, свойство Length используется в условном выражении цикла for, с помощью которого осуществляется копирование. Конечно, это очень простой при- мер, но такая технология может применяться и в гораздо более сложных алгоритмах.
180 Глава 5. Подробнее о типах данных и операторах Минутный практикум ж? I. Справедливо ли утверждение, что при присваивании значения одной перс- \i;' Л менной ссылочного типа, которая ссылается на массив, другой такой же переменной элементы первого массива копируются во второй. 2. Что собой представляет свойство Lena th? Проект 5-2. Класс Queue p'l Qdemo.cs Как вы знаете, структуры данных отличаются по способу организации данных. Простейшей структурой данных является массив, представляющий собой линейный список. Для доступа к элементам массива указывается его имя и индекс элемента. Массивы часто используются в качестве основы при построении более сложных структур данных, таких как стеки (stacks) и очереди (queues). Стек — это список, доступ к элементам которого осуществляется только по принципу FILO (first in, last out — «первым вошел, последним вышел»). Очередь — это список, доступ к элементам которого осуществляется только по принципу FIFO (first in, first out — «первым вошел, первым вышел»). То есть стек можно сравнить со стопкой тарелок — самая нижняя тарелка будет использована последней, а очередь можно сравнить с очередью в банке — кто первым пришел, того первым обслужат. Стеки и очереди удобны тем, что в них средства хранения информации объединены с методами, которые обеспечивают доступ к этой информации. Таким образом, стеки и очереди представляют собой механизмы доступа к данным, в которых хранение и извлечение данных обеспечиваются самой структурой данных без помощи програм- мы. Такое объединение, безусловно, соответствует конструкции класса. В этом проекте будет создан простой класс Queue. В целом очереди поддерживают два базовых метода — put (поместить) и get (извлечь). При выполнении каждого метода put элементу массива, находящемуся в конце очереди (то есть имеющему индекс, соответствующий текущему состоянию очереди), присваивается некоторое значение. При выполнении каждого метода get из элемента массива, находящегося в начале очереди, считывается значение. Значе- ние элемента может быть считано только один раз. Очередь считается заполненной, если отсутствуют свободные элементы для хранения новых значений, и считается пустой, если все значения элементов очереди считаны. Существуют два основных вида очереди — круговая и некруговая. В круговой очереди при считывании значений «освободившиеся» элементы используются повторно. (На самом деле элементы не «освобождаются» — просто в соответствии с алгоритмом работы круговой очереди им «позволено» присваивать новые значения.) В некруговой очереди этого не происходит, поэтому такая очередь в итоге исчерпывает номера индексов своих элементов. Для простоты в данном примере мы создаем нскруговую очередь, но, приложив небольшие усилия, вы сможете преобразовать ее в круговую. 1. Нет. Меняется только ссылка. 2. Length — это свойство, которое имеется в каждом массиве. Оно содержит информацию о количестве элементов, которые могут храниться в массиве.
Использование свойства Length 181 Пошаговая инструкция I. Создайте файл и назовите его QDemo.cs. 2. Существуют различные способы создания класса Queue, но в данном проекте в основе структуры очереди лежат массив и методы, предназначенные для работы с элементами массива. То есть значения, помещаемые в очередь, будут храниться в элементах массива. Доступ к элементам этого массива будет осуществляться с помощью двух индексов. Индекс put.1 сс указывает, какому элементу массива (будет присвоено следующее значение, а индекс qetloc указывает, из какого элемента массива будет считано следующее значение. В соответствии с конструк- цией некруговой очереди после считывания значения какого-либо элемента повторное считывание его значения не допускается. Создаваемая в данном случае очередь будет хранить символы, но такая же конструкция может использоваться для хранения объектов любого типа. Начните создание класса Queue со следую- щих строк кода: class Queue { public char[] q; // Этот символьный массив является основой очереди. public int putloc, getloc; // Индексы, указывающие, какому элементу массива // будет присваиваться значение и из какого // элемента значение будет считываться. 3. Ниже представлен конструктор Queue (), который создает очередь заданной длины: public Queue(int size) { q — new char [ size-rl j ; 1! Выделение памяти для очереди, putloc - getloc - 0; ) Обратите внимание, что длина создаваемой очереди на единицу больше, чем это указано в переменной size. Такое увеличение длины необходимо для реализации алгоритма некруговой очереди, согласно которому один элемент массива не будет использоваться. Следовательно, для соответствия указываемой длины очереди с се истинной «емкостью» размер массива должен быть на единицу больше. Индек- сам putloc и geloc изначально присваивается значение 0. ‘I. Добавьте в код класса метод put О, с помощью которого элементам массива присваиваются значения. // Помещает символ в очередь. public void put(char ch) { if(putloc==q.Length-1) { Console.WriteLine(" - Очередь заполнена."); return; } putloc++; q[putloc] = ch; } Метод начинается с оценки условного выражения, в котором проверяется, запол- нена ли очередь. Если значение переменной putloc равно максимальному индексу массива q, это означает, что в массиве больше нет элементов, которым можно было бы присвоить значение. Если же в массиве еще есть неиспользован- ные элементы, то значение переменной putloc увеличивается на единицу, а новое
। лава э. । юдроонве о типах данных и операторах значение, передаваемое методу put в качестве аргумента, присваивается элементу ; с индексом, равным значению переменной putloc. Следовательно, переменная putloc всегда является индексом последнего помещенного в массив элемента. 5. Для считывания значений элементов используется метод get (), представленный ниже. // Считывает значения элементов массива (очереди). public char get О ( iftgetloc «= putloc) ( ' Console-WriteLine(" - Очередь пустая."); return (char) 0; . 1 | getloc++; 1 return qfgetloc); S } 1 1 I Обратите внимание, что в самом начале с помощью условного выражения I проверяется, помешено в очередь какое-либо значение или нет. Если переменные I getloc и putloc имеют одинаковые значения (то есть являются индексами 1 одного и того же элемента), то очередь является пустой. Поэтому конструктор 1 Queued присваивается значение 0 обеим этим переменным. Затем переменная | getloc увеличивается на единицу, и с помощью оператора return значение 3 соответствующего элемента возвращается подпрограмме, вызвавшей метод. Таким | образом, значение переменной getloc всегда указывает позицию последнего | извлеченного элемента. I 6. Ниже представлен весь код программы QDemo. cs. | /* j Проект 5-2. J В программе демонстрируется использование класса Queue, -5 предназначенного для работы с символами. i *! I using System; | class Queue { й public char[] q; // Этот символьный массив является основой очереди. public int putloc, getloc; // Индексы, указывающие, какому элементу массива // будет присваиваться значение и из какого // элемента будет считываться значение. public Queue(int size) { q = new char[size+1]; // Выделение памяти для очереди, putloc = getloc -= 0; } // Помещает символ в очередь. public void put(char ch) { if (putloc—q.Length-1) { Console.WriteLine(" - Очередь заполнена."); return; ) putloc++; { qtputloc] - ch; |
Использование свойстве Length 183 // Считываем значения элементов массива (очереди). public char get() ( if(getloc == putloc) ( Console.WriteLine(" Очередь пустая."); return (char) 0; ) getloc++; return qlgetloc]; // Демонстрируется использование класса Queue. class QDemo ( public static void Main() { Queue bigQ - new Queue(100); Queue smallQ “ new Queue (4); char ch; int i; Console.WriteLine("Класс bigQ используется для хранения символов "+ " алфавита."); // Помещение 26 символов английского алфавита в очередь bigQ. for(i=0; i < 26; i++) bigQ.put((char) ('A' + i)}; // Считывание и вывод на печать значений элементов очереди bigQ. Console.Write("Содержимое очереди bigQ: "); for(i-0; i < 26; i++) ( ch = bigQ.get(); if(ch != (char) 0) Console.Write(ch); I Console.WriteLine("\n"); Console.WriteLine("Тестирование очереди smallQ."); //В данном цикле выполняется попытка поместить значение // в заполненную очередь. for(i-0; i < 5; i++) ( Console.Write("Попытка поместить в очередь символ " + (char) ('Z' - i)); smallQ.put ( (char) ('Z' - i) ) ; Console.WriteLine(); } Console .WriteLine () ; // В данном цикле выполняется попытка считать значение из пустой очереди. Console.Write("Содержимое очереди smallQ: "); for(i--0; i < 5; i++) ( ch - smallQ.get (); if (ch (char) 0) Console.Write(ch);
1В4 Глава 5. Подробнее о типах данных и операторах 7. В результате работы этой программы на экран будут выведены следующие строки: Класс bigQ используется для хранения символов алфавита. Содержимое очереди oigQ: ABCDE'FGHl JKLMNiJPQRS x’CWvXY3 Тестирование очереди smaliQ. Попытка поместить в очередь символ Z Попытка поместить в очередь символ Y Попытка поместить в очередь символ X Попытка поместить в очередь символ W Попытка поместить в очередь символ V Очередь заполнена. Содержимое очереди smallQ: ZYXW Очередь пустая. 8. Попробуйic самостоятельно модифицировать класс таким образом, чтобы в нем использовались другие типы данных. Например, пусть значения, помещае- мые в очередь, будут иметь тип Int или double. Цикл foreach О том, что в C# определен цикл foremen, мы упоминали в главе 3, а сейчас расскажем о нем подробно. Цикл foreach используется для обработки элементов коллекции. Коллекция — это группа объектов. В C# определены несколько типов коллекций, одним из которых является массив. Синтаксис цикла foreach выглядит так: foreach (type var-nanie in collection) sea cement; Здесь словосочетание type var-naire — это тип и имя итерационной переменной, которая будет принимать значение элементов из коллекции при выполнении цикла foreach. Циклически обрабатываемая коллекция указана словом collection. В приводимых ниже программах используемой коллекцией будет массив. Следова- тельно, тип type должен быть таким же (или совместимым), как базовый тип массива. Обратите внимание, что при работе с массивом итерационная переменная доступна только для чтения. Таким образом, вы не може те измени ть содержимое массива путем присваивания итерационной! переменной нового значения. Ниже представлена простая программа, в которой используется цикл fortach. Вначале создается массив целочисленного типа, элемен там которого присваиваются начальные значения. Затем эти значения выводятся на экран, и одновременно вычисляется их сумма. 1 I //В программе демонстрируется использование цикла foreach. using System; class E’o reach Demo { public static void MainO t int sum - C; int[j nums - new inn [ 1C -; // Элементам массива nums присваивается некоторые начальные значения, for (int i - 0; i < 10; 1-*-+) nums[ i ] i; // Использование цикла foreach для вывода на экран значений
цикл foreach 185 '/ всех элементов массива и подсчета их суммы. Coreach (int х in nums) { <-------—[Обработка массива nums с помощью цикла foreach. Console.WriteLine("Значение элемента массива - " + х) ; ” Console.WriteLine("Сумма значений всех элементов массива - •* + sum); результат выполнения этой программы выглядит следующим образом: * л i учение элемента массива -• О значение элемента массива - 1 Значение элемента массива - 2 Значение элемента массива - 3 Значение элемента массива - 4 Значение элемента массива — 5 Значение элемента массива — 6 Значение элемента массива - 1 Значение элемента массива - 8 Значение элемента массива - 9 Сумма значений всех элементов массива - 4 5 Как видите, обработка элементов массива с помощью цикла foreach производится в порядке возрастания их индексов. С помощью цикла foreach можно также обрабатывать многомерные массивы. При этом значения элементов возвращаются по строкам — от первого до последнего элемента в каждой строке. В программе демонстрируется использование цикла foreach для обработки элементов двухмерного массива. using System; с-ass ForeachDemo2 { public static void Main() { int sum 0; nums - new int[3,5]; // Элементам массива nums присваиваются некоторые значения. for(int i - 0; i < 3; for (int j-C; j < 5; j-*-4-) nurcs[i,j] - (i+l)*(j+1); '/ Использование цикла foreach для вывода на экран значений // всех элементов массива и подсчета их суммы. foreach (int х in nums) { Console. WriteLine ("Значение элемента массива - " -г х) ; sum +-^ х; ) Console.WriteLine ("Сумма значений всех элементов массива - " + sum); Результат выполнения этой программы аналогичен предыдущему. Значение элемента массива - 1 "лачение элемента массива 2 качение элемента массива - 3 значение элемента массива - 4 Значение элемента массива - 5
186 Глава 5. Подробнее о типах данных и операторах Значение элемента массива 2 Значение элемента массива - 4 * Л Значение элемента массива = 6 Ж Значение элемента массива = 8 Значение элемента массива = 10 Значение элемента массива - 3 м. Значение элемента массива -6 Ж Значение элемента массива = 9 Значение элемента массива - 12 « Значение элемента массива - 15 ;+ Сумма значений всех элементов массива 90 Ж Поскольку с помощью цикла f oreach массив можно обрабатывать только от начала до я- конца, может сложиться впечатление, что возможности этого цикла ограничены. Но это г не так. Многие алгоритмы требуют именно такого механизма обработки. Ниже пред- Ш ставлен вариант класса MinMax, с которым мы уже работали в этой главе. Напомним, Я что он предназначен для нахождения минимального и максимального значений элемен- Ж тов массива. /* В программе демонстрируется использование цикла foreach для нахождения минимального и максимального значений элементов массива. */ using System; class MinMax { public static void Main() { int[] nums = { 99, -10, 100123, 18, -978, 5623, 463, -9, 287, 49 }; int min, max; min ~ max = nums[0]; foreach(int val in nums) { if(val < min) min = val; if(val > max) max = val; } Console.WriteLine("Минимальное значение = ” + min + " Максимальное значение = " + max); } } В этой программе цикл foreach работает прекрасно, поскольку нахождение мини- мального и максимального значений требует проверки всех элементов. Кроме того, цикл foreach используется для вычислений среднего значения, поиска значения или элемента и копирования массива. Минутный практикум 1. Какие функции выполняет цикл foreach? 2. Можно ли с помощью цикла foreach осуществлять доступ к элементам массива в обратном порядке — от наибольшего индекса к наименьшему? 3. Можно ли использовать цикл foreach для присваивания значений элементу массива? 1. С помощью цикла foreach осуществляется обработка элементов массива. 2. Нет. 3. Нет.
Строки 187 Строки С точки зрения решения повседневных задач программирования одним из наиболее важных в C# является тип данных string. Во многих языках программирования строка — это массив символов, но в C# строки являются объектами. Следовательно, гип данных string в C# является ссылочным типом. Объекты типа string использовались в программах с первой главы этой книги, потому что при создании строкового литерала фактически создавался объект типа : eng. Например, в следующем операторе Console.WriteLine (”В C# строки являются объектами."); строка в C# строки являются объектами, автоматически становится объектом типа string. Следовательно, класс string в предыдущих программах использовался неявно. В этом разделе мы расскажем, как оперировать такими данными явно. Но поскольку класс string достаточно большой, здесь будут рассмотрены только его основные характеристики. Создание объектов класса String Самым простым способом создания объекта типа string является использование строкового литерала. Например, в представленном ниже операторе переменная str является переменной ссылочного типа string, которой присваивается ссылка на строковый литерал. string str = "Это строковый литерал."; В этом случае переменная str инициализируется символьной последовательностью Это строковый литерал. Вы также можете создать объект типа string из массива типа char. Например, c.-,ar[] str - {'t', 'е', 's', 't'}; string str = new string(str); После создания объекта типа string его можно использовать в любом месте вашей программы, где разрешено применение строки в кавычках. Например, можно ис- пользовать объект типа string в качестве аргумента метода WriteLine О, как показано в следующей программе. ‘ ' В программе демонстрируется создание объектов типа string. -sing System; lass StringDemo { public static void. MainO { chart] charray = {’Э*r ' t’, ’o', ' 'c',.'t', 'p', 'o', 'к', 'a', string strl = new string(charray); string str2 = "Еще одна строка."; V Console.WriteLine(strl); Console.WriteLine(str2); Создание объектов типа string из массива типа char и из строкового литерала.
188 Глава 5. Подробнее о типах данных и операторах В результате работы данной программы будут выведены следующие строки: Это строка. Еще одна строка. Операции со строками Класс string содержит достаточно большое количество методов для работы со строками. Перечислим некоторые из них. static string Copy (st: t ir.g str) Возвращает копию переменной str int Ccr.pareTo(striirg Str) , Возвращает число меньше ноля, если значение вы- зывающей строки меньше значения переменной str; i. возвращает число больше ноля, если значение Bbl- г. зывающей строки больше значения переменной sfr; н- : t . ' возвращает нспь, если строки равны int IndexOf {string Str) int LastlndexOf(string Str) Выполняет поиск в вызывающей строке подстроки, указанной в качестве параметра str. Возвращает индекс позиции в вызывающей строке, где первый раз была обнаружена подстрока, или значение -1, если подстрока не была обнаружена Выполняет поиск в вызывающей строке подстроки, указанной в качестве параметра str. Возвращает индекс позиции, где последний раз былаобнаружена подстрока, или значение -1, если подстрока не была обнаружена Объекты типа string имеют также свойство Length, содержащее информацию о длине строки. Для получения значения отдельного символа строки необходимо использовать его индекс. Например, string str - "test"; Console.WritrLine(str[0]); В результате выполнения второго оператора будет выведен символ t. Как и в массивах, первый элемент строки имеет индекс 0. Но использовать индекс для присвоения символу в пределах строки нового значения нельзя. Индекс может использоваться только для получения символа. Для сравнения двух строк можно использовать оператор ==. Обычно, когда этот оператор применяется к ссылкам на объект, он определяет, на один ли объект они ссылаются. С объектами типа string дело обстоит иначе. Когда оператор == применяется к двум ссылочным переменным типа string, то проверяется содержимое самих строк. То же самое справедливо для оператора !=; когда он используется при сравнении объектов типа string, то сравнивается содержимое самих строк. Однако другие операторы сравнения (например, < или >=) сравнивают ссылки, как и при работе с объектами иных типов. Ниже представлена программа, в которой демонстрируется использование несколь- ких операций со строками. .'/ В программе демонстрируется использование некоторых операций со строками, using System;
Строки 189 C-LdSS Strops { public static void Main() { string strl - "Применение некоторых операций co строками."; string str2 - string.Copy (strl); string scr3 = "Это строковый литерал."; int result, idx; Console. WriteLine ("Длина строки strl - " -t-strl. Length -» " символа."); // С помощью цикла for на экран выводится строка strl // по одному символу за проход цикла. for(int i-C; i < strl.Length; i + + ) Console.Write(strl[i]); Console.WriteLine (); if(strl -- str2) Console.WriteLine("str1 — str2"); else Console.WriteLine ("str1 !- str2"); if (strl - - str3) Console.WriteLine("strl -- str3"); else Console.WriteLine("str1 1= str3”); result = strl . CoinpareTo (st r3 ) ; if (result — 0) Console.WriteLine("Строки strl и str3 равны."); else if(result < 0) Console.WriteLine("Строка strl меньше, чем str3."); else Console.WriteLine("Строка strl больше, чем str3."); // Присвоение нового строкового литерала переменной str2. str2 = "Один Два Три Один."; Console . Wri teLine ( "Один Два Три Один.’’); idx -= str2 . IndexOf ("Один" ) ; Console. Wri teLine ("Индекс первого вхождения подстроки Один " "в строку str2: "» idx); idx г str2.LastindexOf("Один”); Console.WriteLine("Индекс последнего вхождения подстроки Один ” *• "в строку str2: " • юх) ; Результат выполнения программы следующий: Слина сороки strl -= 42 символа. -оименение некоторых операций со строками. tri -== st г 2 tri str3 грека strl меньше, чем str3. 'дин Два Три Один. Индекс первого вхождения подстроки Один в строку str2: 0 Индекс последнего вхождения подстроки Один в строку str2: 13
190 Глава 5. Подробнее о типах данных и операторах С помощью оператора + вы можете выполнить конкатенацию строк (состыковать их вместе). Так, при выполнении блока операторов string strl = "Конка” string str2 — "тен"; string str3 = "ация"; string str4 - strl+str2+str3; переменной str4 присваивается строковый литерал Конкатенация. Массивы строк Как и другие типы данных, строки могут быть значениями элементов массива. Например, в следующей программе значениями элементов массива str являются четыре строковых литерала. // В программе демонстрируется использование массива, значениями элементов // которого являются строковые литералы. using System; Строковый массив. class StringArrays { .s public static void Main() { string [] str - {"Это”, "пример”, "строкового”, "массива.”}; Console.WriteLine("Первоначальный массив: "); for(int i=0; i < str.Length; i++) Console .Write (str [i] + " Console.WriteLine(”\n"); // Изменение значений строкового массива. str[l] - "тоже”; str[21 = "строковый"; str[3] = "массив.”; Console.WriteLine ("Измененный массив: *'); for(int i^O; i < str.Length; i++) Console .Write (str [i] + ” *'); Результат выполнения программы выглядит следующим образом: Первоначальный массив: Это пример строкового массива. Измененный массив: Это тоже строковый массив. Неизменность строк При работе со строками следует обратить внимание на очень важную характеристику объектов типа string — однажды созданная последовательность символов в строке не может быть изменена. Хотя такое ограничение на первый взгляд кажется серьез- ным недостатком, на самом деле это не так. Когда вам потребуется строка, отличная от уже существующей, то вы просто создадите новую строку, содержащую необходимые изменения. Поскольку неиспользуемые строковые объекты автоматически удаляются из памяти во время «сборки мусора», старые строки будут незаметно для вас удалены.
Строки 191 Значение переменной ссылочного типа, которая ссылается на объект типа string, безусловно, можно изменить. Но после создания нового объекта типа string его содержимое изменить уже нельзя. д 1Я изменения строки используется метод SubstringO, возвращающий новую строку, которая содержит копию указываемой части вызывающей строки (строки, которая указывается слева от оператора точка (.)). Поскольку создается новый объект типа st ring, содержащий подстроку, то первоначальная строка остается неизменен- ной. Далее мы будем использовать следующий синтаксис метода Substring (): string Substring (int startindex, int len) Здесь слово startindex — индекс элемента, с которого метод будет начинать копирование значений (символов). А с помощью слова 1еп указывается длина копируемой подстроки. Ниже представлена программа, в которой демонстрируется использование метода SubstringO. // В программе демонстрируется использование метода Substring(). using System; class SubStr { public static void Main() { string orgstr = "Регистрация”; В этом операторе создастся новая строка, содержащая необходимую подстроку. // construct a substring string substrl - orgstr.Substring(0, 7); string substr2 =- orgstr.Substring (6, 5); Console.WriteLine("Строковый литерал orgstr: " + orgstr); Console.WriteLine("Строковый литерал substrl: " + substrl); Console.WriteLine("Строковый литерал substr2: " + substr2); Результат выполнения этой программы следующий: Строковый литерал orgstr: Регистрация С:роковый литерал substrl: Регистр Строковый литерал substr2: рация Первоначальная строка orgstr остается неизменной, а переменные substrl и ubstr2 содержат подстроки. Минутный практикум ( Л I. Справедливо ли утверждение, что в C# все строки являются объектами? '-'\W 2. Каким образом можно получить значение длины строки? 3. Какие функции выполняет метод Substring () ? Да. — Значение длины строки можно получить с помощью свойства Length. Метод SubstringO конструирует (возвращает) новую строку, являющуюся подстрокой вызывающей строки.
192 Глава 5. Подробнее о типах данных и оператор! ^Ответы ......- — -- —j Вопрос. Вы сказали, что после создания объекты типа string остаются' ''неизменными. Понятно, что с практической точки зрения это нс слишком серьезное1 ограничение. Но все-таки, существует ли возможность создавать строку, которую можно изменять? Ответ. Это возможно. В C# предусмотрен класс strrngBuilder, находящий- ся в пространстве имен System. Text. С помощью этого класса создаются строковые объекты, которые можно изменять. Однако для решения большинства задач вам, понадобятся объекты класса string, а нс SrringBurlder. Побитовые операторы В главе 2 были рассмотрены арифметические операторы, операторы сравнения и логические операторы С#. Эти операторы используются чаще всего, но в C# предусмотрены и дополнительные операторы, расширяющие возможности этого языка — побитовые операторы. Побитовые операторы работают непосредственно с битами своих операндов. Операндами побитовых операторов могут быть только целочисленные значения. Эти операторы нс могут использоваться для значений типа bool, float или double либо для объектов каких-нибудь классов. Такие операторы называются побитовыми, поскольку используются для проверки, установки или сдвига битов, которые составляют целочисленное значение. Побито- вые операции важны для решения многих задач программирования на системном уровне (например, когда необходимо получение информации о состоянии устройст- ва). Побитовые операторы перечислены в табл. 5.1. Побитовые операторы AND, OR, XOR и NOT Побитовые операторы and, or, XOR и not обозначаются соответственно, &, |, ~ и Они выполняют те же операции, что и их булевы логические эквиваленты, описанные в главе 2. Отличие состоит в том, что побитовые операторы воздействуют на операнды на уровне битов. В приведенной ниже таблице представлены результаты выполнения каждой операции с использованием единиц и нолей. р q р &q р I q рл q ~ р 0 0 0 0 0 1 1 0 0 1 1 0 0 1 0 1 1 1 1 1 1 1 0 0 Побитовый оператор and можно представить как средство для сброса битов. То есть если какой-либо бит в одном из операндов имеет значение 0, то соответствующий ему бит в результате всегда будет иметь значение 0. Например, 11010011 10101010 &-------- 10000010
^битовые операторы I «JU Таблица 5.1. Побитовые операторы Оператор Описание & 1 Побитовый оператор and Побитовый оператор or Побитовый оператор xor Оператор сдвига вправо Оператор сдвига влево Унарный оператор not В следующей программе демонстрируется использование оператора &. Программа преобразует символы нижнего регистра в символы верхнего регистра с помощью переустановки шестого бита в 0 (сброс шестого бита). Как определено в стандартном наборе символов Unicode/ASCII, значение символов нижнего регистра больше соот- ветствующих значений символов верхнего регистра на число 32. Следовательно, чтобы преобразовать символы нижнего регистра в символы верхнего регистра, достаточно сбросить шестой бит, как это показано в программе. // Программа преобразует символы нижнего регистра в символы верхнего // регистра. using System; class UpCase { public static void MainO { char ch; for(int i-0; i < 10; i++) { ch - (char) (’a’ + i) ; Console.Write(ch); // При выполнении этого оператора сбрасывается шестой бит. ch = (char) (ch & 65503); // Теперь значением переменной ch является // символ верхнего регистра. Console . Write (ch + ’’ ”); В результате выполнения этой программы на экран будут выведены следующие пары символов: d.’v оВ сС dD еЕ fF gG hH il jj Число 65503, используемое в операторе and, имеет следующее двоичное представле- ние: 1111 1111 1101 1111. Следовательно, операция AND оставляет без изменений все биты значения переменной ch, кроме шестого, который устанавливается в 0. Операция AND также используется, когда необходимо определить, установлен или еброшен бит. Например, в приведенном ниже операторе производится проверка. Чтановлен ли четвертый бит в значении переменной status. • -(status & 8) Consol.WriteLine("Четвертый бит установлен."); Значение 8 в этом операторе используется потому, что в двоичном представлении иого числа установлен только четвертый бит (0000 1000). Следовательно, оператор
। лава о. । юдроонее о типах данных и оператору if может иметь положительный результат только в том случае, если в знамени переменной status также установлен четвертый бит. В следующей программе така подход используется для вывода на экран двоичного представления значения тип byte. ! // Программа выводит на экран двоичное представление числа, имеющего тип byte, using System; class ShowBits ( public static void Main() { " int t; j oyte val; " ^^B цикле проверяется каждый бит числа 123^ val 123; for(t=128; t > 0; t = t/2) ( if((val & t) != 0) Console-Write("1 "); else Console.Write("0 "); ’ } ‘ } J } Результат выполнения этой программы следующий: 01111011 j В цикле for с помощью побитового оператора and проверяется каждый бит значения переменной val. Если бит установлен, то на экран выводится 1; если бит сброшен- на экран выводится 0. В проекте 5-3 вы увидите, как можно расширить эту базовую концепцию опроса битов для создания класса, в котором бы определялись и вы во-, дились на экран биты любого значения целочисленного типа. В противоположность оператору AND побитовый оператор OR может использоваться^ для установки битов. Если хотя бы в одном из операндов бит установлен, ТС| соответствующий бит в значении переменной также будет установлен. S 11010011 ! 10101010 11111011 Для преобразования символов верхнего регистра в символы нижнего регистра мы можем задействовать оператор OR, как это показано ниже: /' Программа преобразует символы верхнего регистра в символы нижнего регистра, using System; class LowCase { public static void Main() { char ch; for(int i=0; i < 10; i++) { ch = (char) (’ A' + i); Console.Write (ch); // При выполнении этого оператора устанавливается шестой бит. ch - (char) (ch I 32); // Теперь переменная ch содержит символ // нижнего регистра.
ротовые операторы 195 Console.Write(ch + " ") ; } } Ниже показан результат выполнения программы. Аа Bb Сс Dd Ее Ff Gg Hh li Jj В программе операция OR применяется к каждому значению символа и значению 32, которое в двоичном представлении имеет вид 0000 0000 0010 0000. Следовательно, в пвоичиом представлении числа 32 установлен только шестой бит. Если операндами побитового оператора OR являются число 32 и любое другое число, то в результате мы получаем значение, в котором шестой бит установлен, а все остальные биты остаются неизмененными. В результате выполнения описанных выше действий в этой программе каждый символ верхнего регистра превращается в соответствующий! символ нижнего регистра. При выполнении операции XOR бит устанавливается только в гом случае, если сравниваемые биты различны. Например, 01111111 10111001 Л ............ 11000110 Оператор XOR имеет очень интересное свойство, которое позволяет использовать его для шифровки сообщений. Если этот оператор применить к значениям X и Y, а затем к полученному результату и значению Y (вновь), то после выполнения второй операции будет получено значение х. То есть в последовательности операторов R1 - X Л Y; R2 - R1 Л Y; элемент R2 имеет то же значение, что и X. Таким образом, результатом выполнения последовательности двух операций XOR с использованием одного значения будет первый операнд. Этот принцип можно использовать при создании простой програм- мы для шифрования сообщений, в которой некоторое целочисленное значение будет являться ключом, используемым как для шифровки, так и для расшифровки сооб- щения с помощью оператора XOR. Для шифровки сообщения оператор XOR приме- няется первый раз, в результате мы получаем шифрованный текст. Для расшифровки оператор XOR применяется во второй раз, в результате мы получаем первоначальный текст. Ниже приведен код этой программы. *' i В программе демонстрируется использование оператора XOR для шифровки и ' расшифровки сообщения. using System; ass Encode { public static void MainO { string msg = "Совершенно секретно! Алекс Юстасу."; string enemsg =* string decmsg = int key - 88; Console.Write("Первоначальное сообщение; ");
1УЬ Глава 5. Подробнее о типах данных и оператора -------------------.-------------------------------------------------------- Console.WriteLine(msg); // Шифрование сообщения- 1 for (int i=0; i < msg.Length; i + + ) ] encmsg = encmsg + (char) (msg[i] Л key); j Console.Write("Зашифрованное сообщение: ”); ] Console.WriteLine(encmsg); a // Расшифровка сообщения. 1 forfint i=0; i < msg.Length; ir+) । decmsg = decmsg + (char) (encmsg[i] A key); 1 Console.Write("Расшифрованное сообщение: ”); j Console. WriteLine (decmsg) ; ’i > J } 4 В результате выполнения этой программы на экран будет выведено как зашифрован! ное, так и расшифрованное сообщение: | Первоначальное сообщение: Совершенно секретно! Алекс Юстасу. | Зашифрованное сообщение: ????ИА????хЙ??И?К??ухш???Йх?ЙК?ЙТЩ | Расшифрованное сообщение: Совершенно секретно! Алекс Юстасу. Как видите, в результате выполнения двух операций XOR с использованием одного и того же ключа мы получаем расшифрованное сообщение. 1 я Унарный оператор not изменяет все биты операнда. Например, если некое цслочис-^ ленное значение А имеет битовое представление 1001 0110, в результате выполнения операции ~А мы получим значение с битовым представлением 0110 1001. В следующей программе демонстрируется работа оператора not. На экран выводится двоичное представление числа -34 и двоичное представление значения, полученного: в результате применения к этому числу оператора NOT. // В программе демонстрируется использование унарного оператора NOT. using System; class NotDemo { public static void Main() ( sbyte b - -34; for(int t“128; t > 0; t - t/2) ( if((b 4 t) != 0) Console.Write("1 "); else Console.Write("0 "); } Console.WriteLine(); // Изменение значений всех битов числа на противоположные. b = (sbyte) ~b; for(int t-128; t > 0; t = t/2) ( if((b 4 t) !- 0) Console.Write("1 ”); else Console.Write("0 "); } } }
^битовые операторы 1У/ результат выполнения этой программы имеет вид: ccieccci Операторы сдвига В С₽ существует возможность сдвигать биты, составляющие значение, влево или вправо на заданное число позиций. В Cff определены два оператора сдвига битов: << Сдвиг влево >> Сдвиг вправо Общий синтаксис этих операторов следующий: << num-bits value >> num-bits Здесь слово value — значение, в котором биты сдвигаются на определенное число позиций, указанное вместо словосочетания num-bits. Сдвиг влево приводит к перемещению всех битов указанного значения влево на одну позицию, при этом освободившиеся младшие биты заполняются нолями, а соответ- ствующее количество старших битов теряется. Сдвиг вправо приводит к перемеще- нию всех битов на одну позицию вправо, при этом, если вправо сдвигаются биты беззнакового значения, старшие биты заполняются нолями, а соответствующее количество младших битов теряется. В случае сдвига вправо битов знакового цело- численного значения знаковый бит сохраняется (старшие биты заполняются едини- цами). В представлении отрицательного числа старший бит всегда имеет значение 1. В следующей программе демонстрируется результат выполнения операции сдвига битов как вправо, так и влево. В первой части программы переменной типа int задается начальное значение 1, это означает, что установлен только его младший бит. Далее к этому значению применяется восемь операций сдвига, после выполнения которых на экран выводятся младшие восемь битов. Во второй части программы этой же переменной присваивается значение 128, к которому тоже применяется серия из восьми сдвигов, но уже вправо, с выводом результатов выполнения на экран. ' В программе демонстрируется использование операторов сдвига << и >>. ^sing System; *--.ass ShiftDemo { Public static void Main() { int val •= 1; for (int i - 0; i < 8; i+ + ) { forfint t-128; t > 0; t = t/2) { if ( (val & t) ! = 0) Console.Write("1 ") ; else Console.Write("0 ") ; } Console.WriteLine() ; val = val << 1; // Сдвиг влево. Console.WriteLine(); val - 128; for(int 1=0; i < 8; i++) {
лава ь. Подробнее о типах данных и оператор forlint t-128; t > 0; t - t/2) ( if((val & t) ! - 0) Console.Write("1 "); else Console.Write("0 "); } Console.WriteLine (); val = val » 1; // Сдвиг вправо, t } I Ниже показан результат, демонстрирующий порядок выполнения операций сдвига 00000001 00000010 00000100 00001000 00010000 00100000 01000000 10000000 10000000 1 01000000 00100000 00010000 00001000 00000100 00000010 00000001 “W Ответы Вопрос. Двоичное представление данных основано на степени числа 2.4 Могут ли операторы сдвига применяться для умножения или деления целочисленных'! значений на 2? 1 Ответ. Да. Побитовые операторы могут использоваться для выполнения' очень быстрого умножения или деления на 2. Сдвиг влево удваивает значение. Сдвиг• вправо уменьшает значение в два раза. Учтите, что результат будет верным, если при выполнении операции сдвига биты не сдвигаются за пределы двоичного представле-< ния этого числа. Составные побитовые операторы Все двоичные побитовые операторы могут использоваться одновременно с операто- ром присваивания. Например, в обоих операторах, представленных ниже, перемен- ной х присваивается результат операции XOR, операндами в которой выступают значение переменной х и число 127. х = X л 127; X 127;
побитовые операторы 199 проект 5-3. Класс ShowBits , ghowBitsDemo.cs g этом проекте создается класс ShowBits, позволяющий выводить на экран двоичное представление любого целочисленного значения. Такой класс может успешно ис- потьзоваться в программировании. Например, если вы занимаетесь отладкой кода траппера устройства, то, используя этот класс, сможете наблюдать поток данных в 1Во 11 ч и о м продета вл е н и и. Пошаговая инструкция I. Создайте файл и назовите его ShowB.LtsDerro.cs. 2. Начните создание класса ShowBits с определения конструктора этого класса: class ShowBits { public int numbits; public ShowBits(int n) { numbits = n; В классе ShowBits создаются объекты, предназначенные для отображения ука- занного количества битов. Например, для создания объекта, который будет выводить младшие восемь битов какого-либо целочисленного значения, необхо- димо использовать следующий оператор: ShowBits b = new ShowBits(8); Число битов, которые должны быть отображены, указывается конструктору в качестве аргумента. Его значение присваивается переменной mrmbits. 3. Для вывода битового представления числа в классе ShowBits определен метод show (), код которого показан ниже. public void show(ulong val) { ulong mask = 1; // Сдвиг маски (числа I) в нужную позицию. mask «- numbits-1; int spacer = 0; for(; mask ’ = 0; mask »— 1) { if ( (val & mask) !- 0) Console.Write("1”); else Console.Write("0”); spacer++; if ( (spacer % 8) 0) { Console.Write(” "); spacer = 0; } } Console.WriteLine(); } } Обратите внимание, что метод show() имеет один параметр типа ulong. Но это не означает, что методу show () всегда нужно передавать значение типа ulong. Посколь- ку в C# предусмотрено автоматическое приведение типов, методу show () может быть передано значение любого целочисленного типа. Количество выводимых битов
. —. x'Mpvvnee о типах данных и оператор} указывается значением переменной numbits. После каждых восьми битов мето show () выводит пробел для облегчения чтения двоичных значений длинны битовых комбинаций. 4. Ниже представлен весь код программы ShowBitsDemo. /* Проект 5-3. Класс, который предназначен для отображения двоичного представления целочисленного значения. */ using System; class ShowBits { public inc numbits; ’ public ShowBits(int n) { numbits = n; } public void show (ulong val) { ulong mask =1; ‘ // Сдвиг маски (числа 1) в нужную позицию. mask «= numbits-1; int spacer - 0; for(; mask I- 0; mask >> 1) t ’ if ((val & mask) !•= 0} Console. Write (”1") ; else Console.write(”0"); spacer++; i if ( (spacer % 8) 0) { Console.Write(” ”); Ж spacer == 0; .1 } ..I Console.WriteLine(); // Использование класса ShowBits. class ShowBitsDemo { public static void Main() { ShowBits b = new ShowBits(8); ShowBits i ~ new ShowBits(32); ShowBits li - new ShowBits(64); Console.WriteLine("Двоичное представление числа 123: "); b- show(123) ; Console.WriteLine("\пДнокчное представление числа 87987: "); i.show(87987); Console.WriteLine("\пДвоичное представление числа 237658768: ’’) ; li.show(237658768);
201 * опера .up f // С помощью объекта b класса ShowBits можно вывести на экран // младшие биты любого целочисленного значения. Console. WriteLine (”\пМладшие 8 битов двоичного представления числа ’’ + " 87987: ”); b.show(87987); 5 Ниже показан результат выполнения программы ShowBirsDemo. Двоичное представление числа 123: С1111011 Двоичное представление числа 87987: С.ООСОООО 00000001 01010111 10110011 Двоичное представление числа 237658768: 80000000 00000000 00000000 00000000 00001110 00101010 01100010 10010000 Младшие 8 битов двоичного представления числа 87987: 10110011 а Минутный практикум 1. С какими типами данных могут работать побитовые операторы? * и 2. Какие функции выполняют операторы » и <<? 3. Какая ошибка допущена в данном фрагменте кода? byte val = 10; val = val << 2; Оператор ? Одним из наиболее интересных операторов в C# является оператор ?, который часто используется вместо цепочки операторов if-else, имеющей следующий синтаксис: if (condi tion) var = expression!; else var = express!on2; Здесь значение, присваиваемое переменной var, зависит от оценки условия (condi- - оп), которое управляет оператором if. Оператор ? называется тернарным, поскольку ему требуется три операнда. Синтаксис ею выглядит так: r--.pl ? Ехр2 : ЕхрЗ; 1- Побитовые операторы могут работать с целочисленными типами данных. -• Оператор » производит сдвиг битов вправо, а оператор « производит сдвиг битов влево. J При осуществлении операции сдвига влево значение переменной val преобразуется в тип int. Для обратного присваивания этого значения типа int переменной типа byte сначала необходимо выполнить приведение типа.
। лаиа □. i юдроОнее о типах данных и оператору Здесь элемент Ex.pL является булевым выражением, а элементы Ехр2 и ЕхрЗ — выражения, типы которых должны быть одинаковыми. Обратите внимание использование и расположение двоеточия. Рассмотрим, как работает оператор ?. Вначале оценивается выражение Ex.pl. Бед оно имеет значение true, то выполняется выражение Ех.р2, которое и становится результатом всего выражения ?. Если выражение Ex.pl имеет значение false, ту выполняется выражение ЕхрЗ, которое и становится результатом всего выражения ?. : Рассмотрим пример, в котором переменной absval присваивается абсолютно) значение переменной val. absval - val < 0 ? val; // Получение абсолютного значения переменной val. В этом выражении переменной absval будет присвоено значение переменной val) если значение больше или равно нолю. Если значение переменной val отрицатель! ное, то переменной absval будет присвоено значение переменной val, взятое с< знаком минус. (В результате выполнения оператора «унарный минус» отрицательней значение станет положительным.) Тот же код можно написать с использованием цепочки операторов if-else: if (val < 0) absval - val; else absval = val; В представленной ниже программе с помощью оператора ? выполняется дс двух чисел, но при этом запрещается деление на ноль. //В программе демонстрируется использование тернарного оператора ? для // предотвращения деления на ноль, using System; class NoZeroDiv { public static void Main!) ( int result; forfint i - -5; i < 6; i++) ( result — i !— 0 ? 100 /1:0; •<-------------^Предотвращение деления на ноль, if(i ТО 0) Console.WriteLine("100 /"+!+"="+ result); } } } Ниже представлен результат выполнения этой программы. 100 / -5 - -20 100 / -4 - -25 100 / -3 - -33 100 / -2 - -50 100 / -1 - -100 100 / ТО 100 100 / 2 - 55 100 / 3 - 33 100 / 4 - 25 100 /5-20 Особое внимание обратите на следующую строку кода: result -= i !- 0 ? 100 /1:0;
0ператоР ? 203 Здесь переменной result присваивается результат деления числа 100 на значение переменной 1. Однако это деление производится только в том случае, если значение ^ременной i не равно нолю. В противном случае деление не выполняется, просто переменной result присваивается значение 0. фактически нет необходимости присваивать значение, получающееся в результате исполнения оператора ?, какой-либо переменной. Это значение можно использовать качестве аргумента при вызове метода. Либо, если все выражения имеют тип bool, оператор ? можно использовать в качестве условного выражения в цикле или операторе if. Например, ниже представлена усовершенствованная версия предыду- щей программы. Результаты выполнения обеих программ одинаковы. II в программе демонстрируется использование тернарного оператора ? для // предотвращения деления на ноль. using System; class NoZeroDiv2 { public static void MainO ( for (int i - -5; i < 6; i++) if(i != 0 ? true : false) Console.WriteLine("100 / " + i + " « " + 100 / i); } } Обратите внимание на использование оператора if. Если значение переменной i равно нолю, то результатом оценки условного выражения оператора if будет значе- ние false. Таким образом предотвращается деление на ноль и не выполняется метод WruiteLine (). Во всех остальных случаях деление выполняется.
। лава э. । юдробнее о типах данных и операт Контрольные вопросы 1. Покажите, как объявить одномерный массив, содержащий двенадцать элементов типа double. 2. Покажите, как объявить двухмерный массив 4x5 типа int. 3. Покажите, как объявить невыровненный двухмерный массив, в котором' внешний массив состоит из пяти элементов. 4. Покажите, как инициализировать одномерный массив значениями от 1 до 5. 5. Объясните, как работает цикл foreach. Покажите его общий синтаксис. 6. Напишите программу, в которой для нахождения среднего значения из десяти чисел типа double применяется массив. Используйте любые десять чисел. 7. Измените сортировку в проекте 5-1 таким образом, чтобы производи- лась сортировка массива строк. Продемонстрируйте работу такой про- граммы. 8. В чем состоит различие между методами классов string indexOfO и LastlndexOf ()? 9. Усовершенствуйте предназначенный для шифрования класс Encode таким образом, чтобы в качестве ключа использовалась 8-символьная строка. 10. Могут ли побитовые операторы работать с данными типа double? 11. Покажите, как можно переписать цепочку if-else if(x < С) у - 10; else у - 20; используя оператор ?. 12. Является ли оператор & логическим оператором в следующем фрагменте кода? Почему? bool а, Ь; / / . . . 1 f (а & Ь) ; ...
глава Детальное рассмотрение методов и классов □ Управление доступом к членам класса □ Передача объектов методу □ Возвращение объектов методом □ Использование параметров ref и out □ Перегрузка методов □ Перегрузка конструкторов □ Возвращение значений методом Main() □ Передача аргументов методу Main() □ Рекурсия □ Ключевое слово static
Глава 6. Детальное рассмотрение методов и классов В этой главе мы продолжим изучение классов и методов. Сначала рассмотрим вопросы управления доступом к членам класса, затем расскажем о передаче методам объектов и их возврате, перегрузке методов, далее проанализируем различные формы синтаксиса метода Mair. (), поговорим о рекурсии и функциях ключевого слова static. 'правление доступом к членам класса Класс выполняет две основные функции, обеспечивающие инкапсуляцию данных. Во-первых, он связывает данные с кодом, который манипулирует ими. Преимущество такой связи в классе мы начали использовать еще в главе 4. Во-вторых, класс обеспечивает средства управления доступом к членам класса. Эту характеристику класса мы рассмотрим в данном разделе. В C# доступ к членам класса осуществляется немного сложнее, чем в других языках программирования. В основном используются два базовых типа членов класса -- открытые (public) и закрытые (private). Доступ к члену класса, объявленному как public, может свободно осуществляться кодом, определенным за пределами этого класса. До настоящего момента в примерах этой книги встречались члены класса с типом доступа (модификатором) public. Доступ к членам класса, объявленным как private, может осуществляться только другими методами этого же класса. Код, определенный за пределами класса, может получить доступ к закрытому члену класса, только используя открытые методы того же класса. Ограничение доступа к членам класса является основным принципом объектно-ори- ентированного программирования, поскольку это ограничение защищает объекты от неправильного использования. Разрешая доступ к закрытым членам класса только посредством строго определенного набора методов, мы предотвращаем присвоение данным некорректных значений. Код, определенный за пределами класса, не может непосредственно присваивать значение закрытому члену класса. Способ и время использования данных в пределах объекта также можно строго контролировать. Следовательно, если класс правильно реализован, то он создает «черный ящик», который можно использовать, но вся его внутренняя работа защищена от несанк- ционированного вмешательства. Лодификаторы в C# Управление доступом к членам класса осуществляется посредством четырех модифи- каторов: public, private, protected и internal. В этой главе мы рассмотрим только модификаторы public и private. Модификатор protected применяется лишь в тех случаях, когда задействовано наследование; подробно об этом модифи- каторе мы поговорим в главе 7. Модификатор internal используется в основном Для компоновки программы (файла), проекта или компонента. Краткое описание модификатора internal будет дано в главе 12. Если для члена класса используется модификатор public, то доступ к этому члену класса может осуществляться любым иным кодом программы, включая методы, определенные внутри других классов. Если для члена класса указан модификатор private, то доступ к такому члену класса может осуществляться только другими членами этого же класса. Таким образом, у
управление доступом к членам класса си/ методов одного класса отсутствует возможность доступа к закрытому члену другого класса. Как уже говорилось в главе 4, при отсутствии модификатора член класса автоматически становится закрытым в своем классе. Следовательно, при определении закрытых членов класса использование модификатора private не является обяза- 'рСЛ ьн ы м. В спецификации члена класса модификатор указывается первым (то есть объявление члена класса должно начинаться с модификатора). Приведем несколько примеров: puolic string errMsg; private double bal; private bool isError(byte status) { // ... Чтобы вам легче было понять различие между модификаторами public и private, рассмотрим следующую программу: /! в программе демонстрируется использование модификаторов public и private. using System; class MyClass { private int alpha; // Модификатор private указан явно. int beta; // Модификатор private назначается по умолчанию. public int gamma; // Объявляется переменная с модификатором public. /* Определение методов, осуществляющих доступ к переменным alpha и beta. Члены класса (методы) имеют доступ к закрытым членам этого же класса (переменным). */ public void setAlpha(int а) { alpha - а; } public int getAlpha О { return alpha; i public void setBeta(int a) { beta = a; } public int getBetaO { return beta; } class AccessDemo { public static void Main() { MyClass ob = new MyClass(); /* Доступ к переменным alpha и beta возможен только с помощью методов класса MyClass. */ ob.setAlpha(-99) ; ob.setBeta(19) ; Console .WriteLine ("Значение переменной ob.alpha “ ’’ + ob.getAlpha () ) ; Console.WriteLine("Значение переменной ob.beta = " + ob.getBeta());
। лава в. детальное рассмотрение методов и // Два оператора, приводимые ниже, недействительны, ob.alpha ~ 10; // ob.beta - 9; Неверно 1 Переменная alpha объявлена как private! Неверно! Переменная beta тоже закрытая! Ошибка! Переменные alpha и~ь2! являются закрытыми. j А следующий оператор действительный, поскольку возможен непосредствен! доступ к переменной, объявленной как public._____________ ob.gamma - 99; -4 Верно, поскольку переменная gamma была объявлена с модификатором public. Г 1 Внутри класса MyClass переменная alpha объявлена как private, переменная be имеет модификатор private по умолчанию, а переменная gamma объявлена й public. Поскольку переменные alpha и beta являются закрытыми, доступ к ни помощью кода, определенного за пределами их класса, невозможен. Следователь внутри класса AccessDemo ни к одной из двух указанных переменных не обращаться непосредственно. Доступ к ним можно осуществлять только с использ ванием открытых методов, таких как setAlpha() и getAlpha(). Допустим, е удалить символы комментария в начале оператора // ob.alpha =10; // Неверно! Переменная alpha объявлена как private! то эта программа не будет скомпилирована, поскольку в ней осуществляется попыт непосредственного обращения к закрытой переменной. Но, хотя доступ к перемени alpha не может осуществляться кодом, определенным за пределами класса MyClas’ эта переменная доступна для методов, определенных внутри класса MyClass, та как setAlpha () и getAlpha (). То же самое справедливо по отношению к переме! ной beta. Чтобы продемонстрировать использование модификаторов доступа для реше практических задач, рассмотрим следующую программу, в которой создается масс] типа int. В нем реализована защита от обращений к несуществующим эдеме массива, то есть обрабатываются ситуации, когда указывается индекс, выходящий граничные значения индексов данного массива, — < 0 или > Length-1. Алгор такой обработки мы в дальнейшем будем называть алгоритмом предотвращен^ сбоев, поскольку он предупреждает возникновение ошибки выполнения програм при которой система генерирует соответствующее сообщение, после чего выполне программы прекращается. Реализация этого алгоритма осуществляется посредс инкапсуляции массива как закрытого члена класса, при этом доступ к масси« разрешен только методам, являющимся членами данного класса. При таком поДХСУ можно предотвратить любую попытку доступа к элементу массива по недействите ному индексу. Далее представлен код класса FailSoftArray, в котором реализо упомянутый выше алгоритм. // В этом классе реализован алгоритм предотвращения сбоев. using System; Закрытые переме экземпляра. class FailSoftArray { private int[] a; private int errval; Объявление ссылки на массив. Объявление переменной, которой будет присвоено значение, возвращаемое методом get() в случае выхода за граничные значения индексов массива.
ZU9 вление доступом к членам класса public int Length; Переменная Length объявлена как public. Конструктор создает массива и значение, выхода за граничные массив, получая в качестве параметров значение длины возвращаемое методом get() в случае значения индексов массива. */ ublcc FailSof tArray (int size, a - new int[size]; errval - errv; Length - size; int errv) { // Уетод возвращает значение элемента с индексом, переданным ему в fl качестве параметра. public int get(int index) { if(ok(index) ) return a[index]; <------------------------- return errval; If Метод присваивает значение элементу с указанным // индексом. Если индекс действительный, метод // возвращает булево значение true, в противном // случае — значение false. public bool put (int index, int val) { if(ok(index)) { <-------—---------------------------- a[index] = val; return true; } return false; } Выявление недействительного индекса. // Метод возвращает значение true, если индекс не выходит за граничные // значения индексов массива. private bool ok (int index) ( 4--------------1Закрытый метод. | if (index 0 & index < Length) return true; return false; // Демонстрируется использование массива, в котором реализован алгоритм предотвращения сбоев. class FSDemo { public static void Main() { FailSoftArray fs = new FailSoftArray{5, -1); int x; '{ Согласно определению метода put() при попытке обращения к / несуществующим элементам массива он просто возвращает значение false. Console . WriteLine ("Использование алгоритма предотвращения сбоев.’’); for(int i=0; i < (fs.Length * 2); i+t) fs.put(i, i *10); Console.Write (’’Значения элементов массива: ”); -or(int i=0; i < (fs.Length * 2); i++) { x - fs.get(i) ; if(x ’= -1) Console.Write (x t ’’ ");
I } Console.WriteLine .. // Если при выполнении условного выражения в операторе if имеет место ? /; попытка обращения к несуществующим элементам массива, то выводится | // соответствующее сообщение. j Console.WriteLine("ХпОбработка ошибки с выводом сообщения.”); ) for(int i=0; i < (fs.Length * 2); it+) if(‘fs.put(i, i*10)) Console .WriteLine ("Индекс " + i + ’’ выходит за граничные значения ” + < "индексов данного массива."); Console .Write ("Значения элементов массива: for (int 1^0; i < (fs.Length * 2); i-r-t-) { x - fs.get (i); if(x -1) Console.Write(x + " "); else Console.Write(”\пИндекс ” + i + ” выходит за граничные значения ” + "индексов данного массива."); } 1 Результат выполнения этой программы: Использование алгоритма предотвращения сбоев. Значения элементов массива: 0 10 20 30 40 Обработка ошибки с выводом сообщения. Индекс 5 выходит за граничные значения индексов данного массива. Индекс 6 выходит за граничные значения индексов данного массива. Индекс 7 выходит за граничные значения индексов данного массива. Индекс 8 выходит за граничные значения индексов данного массива. Индекс 9 выходит за граничные значения индексов данного массива. Значения элементов массива: 0 10 20 30 40 Индекс 5 выходит за граничные значения индексов данного массива. Индекс 6 выходит за граничные значения индексов данного массива. Индекс 7 выходит за граничные значения индексов данного массива. Индекс 8 выходит за граничные значения индексов данного массива. Индекс 9 выходит за граничные значения индексов данного массива. Давайте внимательно разберем этот пример. Внутри класса FailSof tArray объяв- ляются три закрытых члена класса. Первый член класса — переменная а, содержит ссылку на массив, в котором будут храниться данные. Второй член класса — переменная errval, хранит значение, которое будет возвращено при обращении метода get () к несуществующему элементу массива. Третьим членом класса является метод ок (), который определяет, не выходит ли индекс за граничные значения индексов данного массива. Все эти три члена класса могут использоваться только другими членами класса FailSoftArray. Остальные члены класса имеют модифи- катор public, го есть могут использоваться любым другим кодом программы, в которой применяется класс FailSoftArray. При создании объекта типа FailSoftArray нужно указать размер массива и значе- ние, которое будет возвращено, если метод get() обратится к несуществующему элементу массива. Возвращаемое значение должно явно отличаться от значений, сохраняемых в массиве. После создания и массив, на который ссылается перемен- ная а, и возвращаемое значение переменной errval становятся недоступными (то
управление доступом к членам класса еСть защищенными от неправильного использования) для пользователей объекта типа р=-: 1 SoftArray. Так, пользователь не может непосредственно указать индекс эле- мента массива а, поскольку этот индекс может оказаться ошибочным. Доступ к объекту возможен только с использованием методов get () и put (). g данном случае метод ок () объявлен как private не из практических соображений, а тишь для демонстрации работы модификатора. Этот метод без всякого ущерба для программы можно было определить как public, так как он не изменяет объект. Но поскольку его использование предусмотрено только внутри класса FailSoftArray, он может быть определен и как private. Обратите внимание, что переменная экземпляра Length объявлена как public, что позволяет непосредственно обращаться к этой переменной для получения значения длины массива FailSoftArray. Чтобы присвоить значение элементу массива FailSoftArray, необходимо вызвать метод put () и указать для него в качестве параметра индекс элемента. Чтобы извлечь из массива значения элемента, необходимо вызвать метод get() и указать индекс элемента. Если указанный индекс находится за пределами допустимых значений, то метод put () возвращает значение false, а метод get () —значение переменной errval. Поскольку по умолчанию члены класса определяются как закрытые, нет необходи- мое! и явно использовать для них модификатор private. Поэтому далее в нашей книге при объявлении закрытых членов класса модификатор private использоваться нс будет. Просто запомните, что если при объявлении члена класса модификатор опущен, то этот член класса будет закрытым. ....- — Вопрос. Рассмотренный выше массив, использующий алгоритм предотвра- щения сбоев, защищен от доступа к несуществующим элементам массива, но это достигнуто за счет изменения обычного синтаксиса индексации (обращения к эле- ментам) массива. Существует ли более эффективный способ создания массива, реализующего подобный алгоритм? Ответ. Да. Из следующей главы вы узнаете, что в C# имеется специальный тип члена класса, индексатор, который позволяет индексировать объект класса так же, как массив. Кроме того, в главе 7 рассматривается более совершенный способ создания переменной Length и работы с ней посредством превращения ее в свойство. Минутный практикум х 1. Назовите модификаторы доступа в С#. 'Л 2. Для чего используются модификаторы private и public? 3. Какой доступ к члену класса устанавливается по умолчанию, если при объявлении члена класса отсутствует модификатор доступа? 1 - Модификаторами доступа в C# являются private, public, protected и internal. 2 Доступ к члену класса, объявленного как private, имеют только другие члены его класса. Член класса, объявленный с модификатором public, доступен и для кода, который определен вне данного класса. Е По умолчанию член класса определяется как private.
Глава 6. Детальное рассмотрение методов и класс< Проект 6-1. Усовершенствованный класс Queue Queue.cs Модификатор private можно использовать для усовершенствования класса Queue з разработанного в главе 5 (проект 5-2). В предыдущей версии все члены класса Queue] являются открытыми, что приводит к определенным проблемам. Раз все члены класса ’ объявлены как public, то код программы, использующей класс Queue, можете получать доступ к массиву этого класса непосредственно и имеет возможность i обращаться к элементам массива вне очереди. Поскольку основная задача очереди — s обеспечение доступа к элементам по принципу FIFO, то обращение вне очереди) нежелательно. Также следует учитывать возможность злонамеренного изменения! значений индексов putloc и getloc, что приведет к неправильной работе очереди, j Вес эти проблемы можно легко решить, сделав часть членов класса Queue закрытыми. ’ Пошаговая инструкция 1. Скопируйте код класса Queue из проекта 5-2 в новый файл с именем Queue.cs. 1 2. В классе Queue удалите модификаторы public из операторов объявления масси- 1 ва q и переменных, предназначенных для хранения индексов putloc и getloc, 1 как показано ниже: 1 // Усовершенствованная версия класса Queue. I class Queue ( ‘ // Теперь эти члены класса по умолчанию становятся закрытыми. J chart] q; // Этот символьный массив является основой очереди, int putloc, getloc; // Индексы, указывающие, какому элементу массива , // будет присваиваться значение и из какого ] // элемента значение будет считываться. t public Queue(int size) { ; q = new char[size+1]; // Выделение памяти для очереди. putloc = getloc = 0; // Помещает символ в очередь. public void put(char ch) { if(putloc==q.Length-1) { Console.WriteLine("- Очередь заполнена."); return; ) putloc++; q[putloc] = ch; } // Считывает значения элементов массива (очереди), public char get О ( if(getloc == putloc) ( Console.WriteLine(Очередь пустая."); return (char) 0; } getloc++; return qfgetloc];
рередача объектов методу Преобразование модификатора членов класса q, putloc и get Loe из открытого в закрытый нс влияет на программу, в которой используется класс Queue. В новой версии мы защищаем класс Queue от возможного неправильного использования. 'Гак. приводимые ниже два оператора, в которых производится попытка непосред- ственного доступа к членам класса, являются недействительными: cueue test - new Queue (10); test.q[0] - 'X'; // Ошибка! test.putloc =• -100; // Ошибка! 4. Теперь члены класса q, putloc и getloc закрытые, а класс Queue строго обес- печивает порядок доступа к элементам очереди по принципу FIFO. Передача объектов методу До этого момента в программах нашей книги в качестве параметров методов исполь- зовались обычные типы значений, такие как int или double. Однако методу можно передавать и объекты. Рассмотрим простую программу, в которой сравниваются параллелепипеды, значения размеров которых хранятся в объектах класса Block. // В программе демонстрируется передача методам объектов в качестве ! параметров. using System; Cu.ass Block { _nt a, b, c; int volume; public Block (int 1, int j, int k) { a = i; b = j; c k; volume = a * b * c; Возвращает значение true, если в объекте ob содержатся '/ те же значения размеров параллелепипеда, / что и в объекте, метод которого был вызван. public bool sameBlock (Block ob) { -4---------------------— if((ob.a == a) & (ob.b b) & (ob.c == c)) return true; else return false; Использование объекта в качестве параметра. // Возвращает значение true, если объем параллелепипеда U объекта ob такой же, как объем параллелепипеда // объекта, метод которого был вызван. ublic bool sameVolume (Block ob) { **— if(ob.volume volume) return true; else return false;
. v. рассмотрение методов и клас< class PassOb ( public static void MainO { Block obi - new Block(10, 2, 5); Block ob2 - new Block(10, 2, 5); Block ob3 - new Block(4, 5, 5); Console. WriteLine (’’Утверждение; объект obi имеет те же значения " +• "переменных, \пчто и объект оЬ2 -"+ obi. same В1 о с k (ob 2) ) ; --------------------- Console.WriteLine("Утверждение: объект obi имеет те же значения " + "переменных, \пчто и объект оЬЗ — ’’ + obi. sameBlock (оЬЗ) ) ; ◄-----------------------------— Console.WriteLine("Утверждение: объем параллелепипеда объекта obi " + "Хправен объему параллелепипеда объекта оЬЗ — " + obi. same Volume (оЬЗ) ) ; ч-——----------------------- Передача методу объекта. Результат работы программы следующий: Утверждение: объект obi имеет те же значения переменных, что и объект ob2 - True Утверждение: объект obi имеет те же значения переменных, что и объект оЬЗ - False Утверждение: объем параллелепипеда объекта obi равен объему параллелепипеда объекта оЬЗ - True В методах sameBlock () и samevolume О происходит сравнение значений перемен- ных вызывающего объекта со значениями переменных объекта, переданного им в качестве аргумента. Метод sameBlock() сравнивает размеры параллелепипедов и возвращает значение true при полной их идентичности. Метод samevolume () сравнивает объемы параллелепипедов, то есть результаты произведений значений всех трех переменных. В обоих случаях обратите внимание, что для параметра ob класс Block указан как его тип. Как видно из этого примера, синтаксически объекты передаются методам тем же способом, что и обычные типы данных. Как передаются аргументы Как показано в предыдущей программе, передача объектов методу является доста- точно простой задачей. Однако в определенных ситуациях передача объекта может заметно отличаться от передачи обычных аргументов. Чтобы понять это, необходимо рассмотреть два способа передачи аргумента подпрограмме. Первый способ называется вызовом по значению. При его использовании копию значения аргумента получает формальный параметр подпрограммы. Следовательно, изменение параметра подпрограммы не оказывает влияния на аргумент, который используется при ее вызове. Второй способ передачи аргумента называется вызовом по ссылке. В этом случае параметру передастся ссылка на аргумент (а не значение аргумента). Внутри подпрограммы эта ссылка используется для доступа к фактиче- скому значению аргумента, который был указан при вызове метода. Это означает, что изменения параметра будут влиять на аргумент, используемый при вызове подпрограммы. В C# применяются оба эти способа передачи аргументов. При передаче методу значений обычных типов, таких как int или double, используется способ передачи по значению. Следовательно, изменение параметра, получившего
ререДача объектов методу 215 аргумент, не влияет на аргумент за пределами метода. В качестве примера рассмотрим слсдуюшУю программу: программе аргументы простых типов передаются по значению. usin-1 System; class x.est „ г* Выполнение этого метода не приведет к изменению переданного ему аргумента. public void noChange(int i, int j) { i i + j; j = "j; } class CallByValue { public static void Main() { Test ob = new Test(); int a 15, b = 20; Console.WriteLine("Значения переменных а и b перед вызовом метода: ” + а + ” и ” + b) ; ob.noChange(a, b); Console-WriteLine ("Значения переменных а и b после вызова метода: " ч- а + " и " + Ь) ; Результат выполнения данной программы будет следующим: Значения переменных а и Ь перед вызовом метода: 15 и 20 Значения переменных а и b после вызова метода: 15 и 20 Как видите, операции, происходящие внутри метода noChange(), не влияют на значения переменных а и Ь, используемых в качестве аргументов при вызове метода. При передаче методу ссылки на объект возникают некоторые трудности. Технически сама ссылка на объект передается по значению, следовательно, параметр получает копию ссылки, что по логике нс должно изменять сам аргумент. (Например, передача параметру ссылки на новый объект не изменит объект, на который ссылается аргумент.) Но, и это весьма существенно, изменение объекта, на который ссылается параметр, приводит к изменению объекта, на который ссылается аргумент. Давайте Рассмотрим, почему так происходит. вспомните, что при создании переменной типа класса (ссылочного типа) вы создаете ссылку на объект, а не сам объект. Объект создается в памяти (ему выделяется память) с помощью оператора new, а ссылка на эту область памяти присваивается переменной ссылочного типа. При использовании переменной ссылочного типа в качестве аргумента для метода параметр принимает ссылку на тот же объект, на который ссылается аргумент. Следовательно, и аргумент, и параметр будут ссылаться на один 11 гот же объект. В действительности это означает, что объекты передаются методу с
216 Глава 6. Детальное рассмотрение методов и класс< использованием вызова по ссылке. Изменения объекта внутри метода влияют на. объект, используемый в качестве аргумента. Рассмотрим следующую программу: : // В программе демонстрируется передача объектов по ссылке. ; i' using System; class Tesr { public int a, b; public Test(int i, int j) { a - i; , b - j; , } < /* Передача методу объекта. Теперь значения переменных ob.a и ob.b в ? объекте, используемом при вызове метода, будут изменены. */ п public void change(Test ob) { J ob.a = ob.a + ob.b; ob.b =- -ob.b; { J : } j class CallByRef { * public static void Main() { г Test ob = new Tesu(15, 20); Console.WriteLine("Значения переменных ob.a и ob.b перед вызовом метода: ” ’ + ob. а + " и ” + ob. b) ; j ob.change(ob); J Console.WriteLine("Значения переменных ob.a и ob.b после вызова метода: “ | + ob. а т " и " * ob.b) ; , } н } В результате работы программы будет выведена следующая информация: д Значения переменных ob.a и ob.b перед вызовом метода: 15 и 20 ’j Значения переменных ob.a и ob.b после вызова метода: 35 и -20 ц Как видите, при выполнении метода change (} были изменены значения переменных1 объекта, а значит, и сам объект, используемый в качестве аргумента. Итак, при передаче методу в качестве аргумента ссылки на объект сама ссылка передается с использованием вызова по значению, следовательно, создается копия этой ссылки. Но поскольку передаваемое значение ссылается на объект, копия этого значения ссылается на тот же объект, на который ссылается соответствующий аргумент. Использование параметров с модификаторами ref и out По умолчанию обычные типы значений, такие как int или char, передаются методу по значению. Это означает, что изменение параметра, который принимает обычный тип значений, не повлияет на аргумент, используемый в вызове. Но если применить
^пользование параметров с модификаторами ref и out 217 кдючсвыс слова ref и out, то можно передать любое значение обычного типа по сСЫткс, что позволит методу изменить аргумент, используемый в вызове. Прежде чем перейти к рассмотрению механизма использования ключевых слов ref и т, необходимо выяснить, зачем может понадобиться передача значений обычных типов по ссылке. В этом возникает необходимость в двух случаях: во-первых, когда нчдо позволить методу изменять содержимое своих аргументов, во-вторых, когда надо позволить методу возвращать более одного значения. Давайте рассмотрим оба эти варианта подробно. Очень часто возникает необходимость в использовании метода, способного изменять значения передаваемых аргументов. Наиболее типичным примером такого метода является метод swap(). Поскольку по умолчанию в C# обычные типы аргументов передаются по значению, нельзя создать метод, который переставит местами значе- ния двух аргументов типа int. Для решения этой проблемы применяется модифи- катор ref. Как вы знаете, оператор return позволяет методу возвращать значение вызывающей подпрограмме. Однако метод может возвращать только одно значение при каждом его вызове. Если же перед вами стоит задача возвратить две и больше единицы информации, то ее нельзя решить с помощью такого метода. К примеру, когда требуется создать метод для вычисления площади прямоугольника и определения, является ли этот прямоугольник квадратом, нужно, чтобы метод возвращал две единицы информации — площадь прямоугольника и значение, указывающее на равенство всех сторон. Для решения этой проблемы применяется модификатор out. Использование модификатора ref В C# использование модификатора параметра ref приводит к созданию вызова по ссылке вместо вызова по значению. Модификатор ref указывается при объявлении метода и при его вызове. Давайте рассмотрим простой пример. В следующей программе создается метод sqr(), возвращающий значение своего аргумента, воз- веденное во вторую степень, которое замещает начальное значение аргумента. Обратите внимание, что при объявлении метода модификатор ref предшествует типу параметра, а при вызове метода ключевое слово ref указывается непосредственно перед аргументом. Ь программе демонстрируется использование модификатора ref для передачи методу значения обычного типа по ссылке. using System; c-ass RefTest { !' Этот метод изменяет значение своего аргумента. Р-’Р11с void sqrfref int i) { •<- Здесь модификатор ref указывается в начале объявления параметра. '--ass RefDemo ( Public static void Main() { RefTest ob - new RefTest (); int a 10;
..««за w. /двшльное рассмотрение методов и классов Console-WriteLine ("Значение переменной а перед вызовом метода: " +• а) ; ______________________________Здесь модификатор ref or, sqr (ret a) ; 4 - - - предшествует аргументу. Console .WriteLine ("Значение переменной а после вызова метода: *’ + а); } Результат выполнения этой программы, представленный ниже, подтверждает, что метод sqr () действительно модифицировал значение аргумента — переменной а. Значение переменной а перед вызовом метода: 10 Значение переменной а после вызова метода: 100 Использование модификатора ref делает возможным создание метода, который будет менять местами значения своих аргументов (значения обычного типа). Ниже представлена программа, которая содержит метод swap (), меняющий местами зна- чения двух целочисленных аргументов, передаваемых ему при вызове. // Программа меняет местами значения двух переменных. using System; class Swap { // Этот метод изменяет значения двух своих аргументов. public void swap(ref int a, ref int b) { int t; t = a; a = b; b -= t; } } class SwapDemo { public static void Main() { Swap ob = new Swap(); int x = 10, у = 20; Console.WriteLine ("Значения переменных x и у перед вызовом метода: " + х + " и " + у) ; ob.swap(ref х, ref у); Console.WriteLine("Значения переменных х и у после вызова метода: ’* + х + " и ” т у) ; ) 1 Результат выполнения этой программы: Значения переменных х и у перед вызовом метода: 10 и 20 Значения переменных х и у после вызова метода: 20 и 10 Обратите внимание, что значение аргументу, передаваемому с модификатором ref, должно быть присвоено до вызова метода. Это важно, поскольку принимающий такой аргумент метод предполагает, что параметр ссылается на действительное значение. Следовательно, применяя модификатор ref, нельзя использовать метод для присваи- вания аргументу начального значения.
Использование параметров с модификаторами ref и out 219 использование модификатора out Иногда параметр ссылочного типа используется лтя получения значения из метода, а нс для передачи ему значения. Метод может выполнять некоторую функцию (например, открытие сокета) и возвращать в параметре ссылочного типа какое-либо значение, определенное для указания успешного или неудачного выполнения опера- ции. Для этой цели в C# предусмотрен модификатор параметра out. При объявлении метода в круглых скобках указывается не информация, которую нужно передать методу, а информация, которая будет получена из метола. Необходимость использо- вания еще одного модификатора, отличающегося от ref, объясняется тем, что параметр с модификатором ref должен быть инициализирован до вызова метода. Следовательно, только для выполнения данного условия необходимо присвоить аргументу некоторое (произвольное) значение. Отличие между модификаторами ref и out заключается в том, что параметр с модификатором out может использоваться только для передачи значения из метода. При этом нет необходимости до вызова метода присваивать переменной начальное значение. Более того, внутри метода параметр с модификатором out всегда рассмат- ривается как не имеющий начального значения. Метод обязан присвоить значение параметру, прежде чем передаст управление программой вызывающей подпрограмме. То есть после вызова метода параметр out всегда будет иметь значение. В качестве примера рассмотрим программу, в методе которой используется параметр с модификатором out. Метод rectlnfoО возвращает значение площади прямо- угольника, для которого заданы размеры его сторон. В параметре isSquare метод возвращает значение true, если прямоугольник является квадратом, и значение false в противном случае. Таким образом, метод rectlnfo () возвращает подпро- грамме, которая его вызывает, две единицы информации. // В программе демонстрируется использование модификатора параметра out. using System; class Rectangle ( '/ Переменные содержат значения длины сторон прямоугольника. - nt sidel; ent side2; public Rectangle(int i, int j) i sidel = i; side2 = j; // метод возвращает значение площади прямоугольника и определяет, является '/ли он квадратом. public int rectlnfo(out bool isSquare) { if (;si c:el --sl c:e2) isSquare - true; else isSquare = false; Передача информации из метода с использованием параметра с модификатором out. return sidel * side2; l I
г.гх> Глава 6. Детальное рассмотрение методов и классов class SwapDemo { public static void MainO { Rectangle rect new Rectangle(10, 23); int area; bool isSqr; area - rect.rectlnfo(out isSqr); if (isSqr) Console.WriteLine("Данный прямоугольник является квадратом."); else Console.WriteLine("Данный прямоугольник не является квадратом."); Consoie.WriteLine("Площадь прямоугольника - " + area т "."); Обратите внимание, что до вызова метода rectlnfo () параметру isSqr нс присваи- вается начальное значение. Если бы параметр метода rectlnfo () имел модификатор’ это было бы невозможно. После завершения работы метода параметр isSqri содержит либо значение true, либо значение false в зависимости оттого, является: прямоугольник квадратом или нет. Значение площади прямоугольника возвращается' с помощью оператора return. Результат выполнения этой программы выглядит, следующим образом: Данный прямоугольник не является квадратом. Площадь прямоугольника = 230. Минутный практикум < I. Чем различается вызов по значению и вызов по ссылке? 2. Как в C# передаются значения обычных типов? Как передаются объекты? 3. Какие функции выполняет модификатор ref? Чем он отличается от моди- фикатора out? -йшелм/ профессионала---------------------..— ..... .....- - — Вопрос. Могут ли модификаторы ref и out применяться с параметрами ссылочного типа, например, при передаче ссылки на объект? Ответ. Да. При использовании модификаторов ref и out для параметров ссылочного типа сама ссылка передается по ссылке. Это позволяет методу изменить направление ссылки, то сеть после выполнения этого метода переменная будет ссылаться уже на другой объект. Рассмотрим следующую программу: /•' В программе демонстрируется использование модификатора ref при вызове '/ по ссылке. using System; class Test { public int a; 1. При вызове по значению подпрограмме передается копия аргумента, а при вызове по ссылке - ссылка на аргумент. 2. В C# аргументы обычных типов передаются но значению, а объекты — по ссылке. 3. Модификатор ref создает вызов по ссылке для параметров обычных типов. Модификатор out также создает вызов по ссылке, но он нс используется для передачи информации методу.
Использование переменного количества аргументов 221 public Test(int i) { a i; /; При выполнении этого метода аргумент не будет изменен. DUblic void noChange(Test о) { Test newob = new Test(O); о — newob; // Выполнение данного оператора не оказывает влияния на объект // "о" вне метода noChangeO . / .* После выполнения этого метода переменная ”о” будет ссылаться на друх’ой /,' объект. public void change (ref Test о) i Test newob - new Test(O); о - newob; // Этот оператор изменит аргумент. class CallObjByRef { public static void Main() { Test ob - new Test(100); Console.WriteLine("Значение переменной ob.a после инициализации объекта: " н ob.a); ob.noChange(ob); Console.WriteLine("Значение переменной ob.a после вызова метода " + "noChangeO: " + ob.a); ob.change(ref ob); Console.WriteLine("Значение переменной on.a после вызова метода " + "change(): ” + ob.a); Ниже представлен результат выполнения этой программы. Значение переменной ob.a после инициализации объекта: 100 Значение переменной ob.a после вызова метода noChangeO: 100 Значение переменной ob.a после вызова метода change(): 0 В этой программе переменной о присваивается ссылка на новый объект внутри метода noChange (), что не влияет на объект оЪ внутри метода Main (). Однако внутри метода change (), в котором используется параметр с модификатором ref, присвое- ние переменной о ссылки на новый объект приводит к тому, что внутри метода ; г, () переменная ob будет ссылаться уже на другой объект. Использование переменного количества аргументов При создании метода обычно заранее известно количество аргументов, которые будут е'’У передаваться. Но это бывает не всегда, иногда требуется метод, которому можно Псредавать произвольное количество аргументов. В качестве такого примера рассмот- рим метод, в котором производится поиск наименьших значений. Этому методу может быть передано два, три, четыре значения и т. д. С использованием обычных
£££ Глава 6. детальное рассмотрение методов и классу параметров его создать нельзя. В данном случае необходимо задействовать параме-ц с модификатором params, с помощью которого можно передавать методу произвол^ ное количество параметров. Модификатор params используется для объявления в качестве параметра массива который будет способен принимать ноль и больше аргументов. Число элементов] массиве будет равным числу аргументов, передаваемых метолу. Следовательно, дл^ получения аргументов программа должна будет обращаться к массиву. В приведенном ниже примере используется модификатор params для создан^ метода rninVal (), который возвращает минимальное значение из нескольких, пере данных в качестве аргументов. // в программе демонстрируется использование модификатора params. using System; class Min { public int minVal(params int(] nums) { <- inc m; Создание параметра переменной длины с использованием моди- фикатора params. iг(nums.Length == 0) { Console.WriteLine("Ошибка 1 Для метода не указаны аргументы."); return 0; m - nums[0]; for(int i=l; i < nums.Length; i++) if(nums[i] < m) m = nums[i); return m; class ParamsDemo { punlic static void Main() { Min ob = new Min () ; int min; int a = 10, b = 20; // Вызов метода с двумя аргументами. min - ob.minVal(a, b); Console.WriteLine("Минимальное значение — " + min) ; // Вызов метода с тремя аргументами. min = ob.minVal(a, b, -1); Console.WriteLine("Минимальное значение " + min); /' Вызов метода с пятью аргументами. min = ob.minVal(18, 23, 3, 14, 25); Console.WriteLine("Минимальное значение = " 4 min); // Методу в качестве аргумента также может быть передан целочисленный // массив. int [ J args = { 45, 67, 34, 9, 112, 8 }; min - ob.minVal(args); Console.WriteLine ("Минимальное значение " 4- min) ;
-^пользование переменного количества аргументов 223 Программа выводит следующий результат: минимальное значение - 10 Минимальное значение - -I Минимальное значение - 3 Минимальное значение - 8 каждый раз при вызове метода minVal () ему передаются аргументы с помощью уасспва nums. Длина массива равна количеству элементов. Следовательно, метод min'.'aJ О можно использовать для поиска минимального значения из любого коли- чества аргументов. Хотя параметру с модификатором params можно передавать любое число аргументов, все они должны быть совместимы с типом массива, определенного в качестве параметра. Например, вызов метода minVal () rain ob. minVal(1, 2.2); является неверным, поскольку значение double (2.2) не конвертируется автоматиче- ски в тип int, который имеет массив nums в методе minVal (). При использовании параметра с модификатором params необходимо контролировать число элементов массива, поскольку параметр с этим модификатором может прини- мать любое число аргументов (даже ноль). В частности, приведенные ниже операто- ры. в которых вызывается метод minVal (). являются синтаксически правильными. mir. ob.minVal(); // Вызов метода без аргументов. min - ob.minVal(3); // Вызов метода с одним аргументом. Поэтому в методе minVal () перед попыткой обращения к первому элементу массива проверяется, есть ли в массиве хотя бы один элемент. В случае отсутствия такой проверки при вызове метода minVal() без аргументов возникнет исключительная ситуация, поскольку произойдет ошибка выполнения программы. (Далее при описа- нии исключительных ситуаций мы расскажем о более эффективном способе защиты от такого рода ошибок.) Код метода minVal () написан таким образом, что разреша- ется производить его вызов с одним аргументом. При этом возвращается этот единственный аргумент. Метод может иметь обычные параметры и параметр переменной длины. В следующем примере метод showArgs () принимает один параметр типа string, а затем параметр переменной длины (с модификатором params) — массив типа int: Ь программе демонстрируется использование обычного параметра и параметра // переменной длины. usir,q System; Class MyClass { paolic void showArgs(string msg, params int[J nums) ( Console.Write(msg M toreach(int i in nums) ------------------------ Console Write (i + " ")• Этот метод имеет один обычный ' параметр и один параметр с мо- дификатором params. Console.WriteLine(); ss ParamsDemo2 {
i лвва о. детальное рассмотрение методов и классу public static void Main() { MyClass ob ~ new MyClass (); Я ob.showArgs("В этом методе кроме строки есть еще 5 аргументов: ", Я 1, 2, 3, 4, 5); -Я оо.showArgs("А в этом методе кроме строки есть еще 2 аргумента: ", -Я 17, 20); Я } fl Результат выполнения программы следующий: Я В этом методе кроме строки есть еще 5 аргументов: 12 3 4 5 А в этом методе кроме строки есть еще 2 аргумента; 17 20 .Д В тех случаях, когда метод имеет обычные параметры и параметр с модификатором® params, параметр переменной длины должен в списке параметров располагаться® последним. Кроме того, в любом случае у метода может быть только один параметр® с модификатором params. Я @ Минутный практикум Я I • Как создать параметр, который принимает переменное количество аргументов?® & 2- Может ли метод иметь более одного параметра params? Я 3. Справедливо ли утверждение, что параметр params может располагаться в® любом месте списка параметров? ж Возвращение объектов I Метод может возвращать любые типы данных, включая объекты какого-либо класса. В следующей программе для сообщения об ошибке используется класс ErrorMsg, в-| котором определен строковый массив. Каждая строка представляет собой краткую -’! характеристику ошибки. На основании кода ошибки, полученного в качестве аргу- | мента, метод getErrorMsg () возвращает значение элемента массива — объект типа | string. // В программе демонстрируется возврат объектов. 5 using System; class ErrorMsg t string[) msgs = { "Ошибка вывода.", К "Ошибка ввода.", .4 "Недостаточно свободного места на диске.", "Выход за граничные значения индексов массива." 1. Для создания параметра, который принимает переменное количество аргументов, исполь- зуется модификатор params. 2. Нет. 3. Нет. Параметр params должен стоять последним в списке параметров.
I*-/1 trio uwUtnivD j/ Возврат сообщения об ошибке - public string getErrorMsg(int i) i 4---------- it(i >=0 & i < msgs.Length) return msgsfi}; else return "Введен неправильный код ошибки."; Возврат объекта типа string, class ErrMsg { public static void Main() ( ErrorMsg err = new ErrorMsg (); Console.WriteLine(err.getErrorMsg(2)); Console.WriteLine(err.getErrorMsg(19)); 1 В результате работы этой программы будут выведены следующие строки: Недостаточно свободного места на диске. Введен неправильный код ошибки. Таким же образом можно возвращать объекты создаваемых вами классов. Например, ниже представлена обновленная версия предыдущей программы, в которой создаются два класса, предназначенные для обработки ошибок. Один класс, инкапсулирующий сообщение об ошибке с кодом «серьезности» ошибки, называется Err. Второй класс содержит метод get Errorinfo (), который возвращает объект типа Err. Этот класс именуется Error inf о. // В программе демонстрируется возврат объектов, определенных пользователем. using System; class Err { public string msg; // Сообщение об ошибке. public int severity; // Переменная, содержащая код серьезности ошибки. public Err(string m, int s) { msg = m; severity - s; class Errorinfo { string [] msgs = { "Ошибка вывода.", "Ошибка ввода.", "Недостаточно свободного места на диске.", "Выход за граничные значения индексов массива." } . ----------------------------------------------------------------------- ' , JВозвращение объекта типа Err. int[] howbad - { 3, 3, 2, 4 }; -----——---------------— public Err getErrorlnfo(int i) { if (i >-0 & i < msgs.Length) return new Err(msgs[i], howbad(ij); else return new Err("Введен неправильный код ошибки.", 0);
Глава 6. Детальное рассмотрение методов и кла| class Errlnfo { public statue void MainO { Errorinfo err - new Errorlnfo(}; Err e; e - err.getErrorInfо(2); Console.WriteLine(e.msg + " Серьезность ошибки: ’’ 1 e. severity); e - err.getErrorInfo(19); Console.WriteLine(e.msg t- " Серьезность ошибки: ” -r e.severity); Эта программа выведет на экран следующую информацию: Недостаточно свободного места на диске. Серьезность ошибки: 2 Введен неправильный код ошибки. Серьезность ошибки: О Каждый раз при вызове метода getError inf о О создается новый объект типа Err, а ссылка на него возвращается вызывающей программе. Затем этот объект исполь- зуется в методе MainO для вывода на экран сообщения об ошибке и кода «серьез- ности» ошибки. Объект, возвращенный методом, существует до тех пор. пока он упоминается в программе, его удаление будет выполнено только при «сборке мусора». Таким образом, объект не может быть уничтожен только потому, что создавший его метод возвратил управление программой вызывающей подпрограмме. Перегрузка метода Данный раздел познакомит вас с одним из самых замечательных свойств C# — перегрузкой методов. В этом языке два и более методов в пределах одного класса могут иметь одинаковые имена при условии, что объявления параметров в методах различаются. (Необходимо, чтобы в методах были указаны параметры различных типов, или методы должны различаться количеством параметров.) В этом случае методы называются перегруженными, а процесс определения метода с таким же именем называется перегрузкой метода. Перегрузка методов является одним из способов реализации полиморфизма в С#. В общем случае для перегрузки метода вам достаточно объявить другую его версию, а компилятор позаботится обо всем остальном. Безусловно, перегружаемые методы могут отличаться и по типу возвращаемого значения, но этого недостаточно, чтобы два метода можно было считать различными. Типы возвращаемых значений не содержат достаточного количества информации для того, чтобы компилятор мог определить, какой из методов необходимо использовать. При вызове перегружаемого метода выполняется тот метод, параметры которого совпадают с аргументами. Ниже представлена простая программа, в которой выполняется перегрузка метода. ! В программе демонстрируется перегрузка метода. using System; c.c:ss Overload f
ререгРУзка метода public void ovlDemo () { <---------------------------111срвая версия метода. Console. Wri teLine ( "Этот метоп не имеет параметров."); / перегруженный метод ovlDemo, версия с одним параметром типа int. public void ovlDemo (int a) { ◄----—-----—------[Вторая версия метода.| Console.WriteLine("Метод с одним параметром: ” + а); } >• Перегруженный метод ovlDemo, версия с двумя параметрами типа int. пиетЮ int ovlDemo (int a, int b) { ◄------------—] Третья версия метода?] Console.WriteLine("Метод с двумя параметрами: ” + а т " и " + Ь); return а + Ь; // перегруженный метод ovlDemo, версия с двумя параметрами типа double, public double ovlDemo (double a, double b) {<—IЧетвертая версия мсгода. I Console.WriteLine("Метод с двумя параметрами типа double: " + a + ” и " + b) ; return a + b; class OverloadDemo { public static void Main() { Overload ob = new Overload(); int resl; double resD; i Вызов всех версий метода ovlDemo(). ob.ovlDemo() ; Console.WriteLine() ; ob.ovlDemo(2) ; Console.WriteLine(); resl ob.ovlDemo(4, 6); Console. WriteLine (’’Результат выполнения метода ob. ovlDemo (4, 6): " + resl); Console.WriteLine(); resD ob.ovlDemo(1.1, 2.32); Console.WriteLine("Результат выполнения метода оо.ovlDemo(1.1, 2.32): " resD); в результате выполнения всех этих методов программы будет выведена следующая информация: ^тот метод не имеет параметров. Метод с одним параметром: 2 Метод с двумя параметрами: 4 и 6 Результат выполнения метода ob.ovlDemo(4, 6): 10
Метод с двумя параметрами типа double: 1,1 и 2,32 Результат выполнения метода ob-ovlDemo(1.1, 2.32): 3,42 В данной программе метод ovlDemoO перегружен четыре раза. Первая версия принимает ни одного параметра, вторая версия принимает один целочисленный' параметр, третья версия принимает два целочисленных параметра, а четвертая версия принимает два параметра типа double. Обратите внимание, что первые две версии имеют возвращаемый тип void, а вторые две — возвращают значения. Такая' программа является действительной. Но мы уже говорили, что перегруженные методы отличаются нс только типом возвращаемого значения, поэтому попытка использо- вания следующих двух версий приведет к ошибке: // Если в программе присутствует только один метод ovlDemo(int), ; ! f она будет работать. s public void ovlDemo(int a) { ------.—.—.—.— -----------—.—.———.— ( Console . WriteLine (’’Метод с одним параметром: ” + a) ; 1 l -----------J------------J Типы возвращаемых значений Ц нс используются для опредсле- я /х Омибка I Два метода ovlDemo(int) должны отличаться имя различия между перегру- 1 не только типом возвращаемого значения- же иными методами._ 2 /’ _ public int ovlDemo(int а) { ... „............... ... , „1 *| Console.WriteLine (’’Метод с одним параметром: ” + а) ; « return а * а; $ } 1 В комментарии говорится, что различие в типе возвращаемых значений является | недостаточным для перегрузки метода. В главе 2 мы рассказывали, что в C# обеспечиваются автоматические преобразования 1 определенных типов данных. Эти преобразования также применяются к параметрам перегруженных методов. Рассмотрим следующий пример: !; В программе демонстрируется автоматическое преобразование типов аргументов. 1 using System; class Overload? { public void f(int x) { Console. WriteLine (’’Значение переменной x внутри метода f (int) : " + x) ; } public void f(double x) { Console. WriteLine (’’Значение переменной x внутри метода f (double) : ” + x) ; i } class TypeConv { public static void Main() { 0verload2 ob = new Overload2(); int i - 10; double d =- 10.1; byte b = 99; short s -- 10; float f - 11.5F; ob.f(i); // Вызывается метод ob.f(int)
Перегрузка метода ob.f(d); // Вызывается метод ob.f(double) ob.f(b); // Вызывается метод ob.f(int) и выполняется автоматическое // преобразование типа аргумента. ob.f(s); // Вызывается метод ob.f(int) и выполняется автоматическое // преобразование типа аргумента. ob.f(f); // Вызывается метод ob.f(double) и выполняется автоматическое // преобразование типа аргумента. Ниже представлен результат выполнения программы. Значение переменной х внутри метода f (int): 10 Значение переменной х внутри метода f(double): 10,1 Значение переменной х внутри метода f(int): 99 Значение переменной х внутри метода f(int): 10 Значение переменной х внутри метода f(double): 11,5 В этом примере определены только две версии метода f (). В одной из них параметр имеет тип int, а в другой — double. Однако в C# существует возможность передать методу f () значение типа byte, short или float. При передаче методу значения типа byte или short C# автоматически преобразует его в тип int. При передаче значения типа float его тип преобразуется в double и вызывается метод f (double). Важно понимать, что автоматическое преобразование типов применяется только при несовпадении типов параметра и аргумента. А вот ешс одна версия предыдущей программы, в которую добавлен метод f () с параметром типа byte. // Еще одна версия предыдущей программы, в которую добавлен метод f(byte). using System; class Overload2 { public void f(byte x) { Console. WriteLine ("Значение переменной x внутри метода f (byte) : " -r x) ; public void f(int x) { Console. WriteLine ("Значение переменной x внутри метода f(int): ” t- x) ; public void f(double x) { Console.WriteLine("Значение переменной x внутри метода f(double): ” + x); } class TypeConv { public static void Main() { Overload2 ob = new Overload2(); int i = 10; double d 10.1; byte b -- 99; short s = 10; float f = 11.5F; ob.f(i); // Вызывается метод ob.f(int) ob.f(d); // Вызывается метод ob.f(double)
Глава 6. Детальное рассмотрение методов и классов ob.f(b); // Вызывается метод ob.f(byte). Теперь автоматическое /,/ преобразование типа не выполняется. ob. f (s) // // ob.f(f); -' / // Вызывается метод ob.f(int) и выполняется автоматическое преобразование тика аргумента. Вызывается метод ob.f(double) и выполняется автоматическое преобразование типа аргумента. Результат выполнения этой программы отличаются от предыдущего только тем, что в третьей строке упоминается метод f (byte), а нс метод f (int). Значение переменной х внутри метода Значение, переменной к внутри метода Значение переменной х внутри метода Значение переменной х внутри метода Значение переменной х внутри метода f (int) : 10 f(double 1 : 10,1 f(byte): 99 f(int): 10 f(double): 11,5 Так как существует версия метода f (byte), при вызове метода f о с аргументом типа byte вызывается метод f (byte) без преобразования типа значения аргумента в int. При перегрузке методов также могут использоваться модификаторы ref и out. В следующем фрагменте кода определены два разных метода: punlic void f(int х) { Console.WriteLine("Значение переменной x внутри метода f(int): " + х); public void f(ref int x) { Console.WriteLine("Значение переменной x внутри метода f (ref int): ” -I x) ; Таким образом, оператор ob.f (i) ; вызывает метод f (int x), но оператор ob.f(ref i); вызывает метод f (ref int x). Перегрузка методов поддерживает полиморфизм, поскольку является одним из способов реализации в СТ принципа «один интерфейс, множество методов». В тех языках, которые не поддерживают перегрузку методов, каждому методу необходимо присвоить уникальное имя. Но довольно часто возникает ситуация, когда требуется применить один метод для различных типов данных. Рассмотрим функцию возврата абсолютного значения. В языках, не поддерживающих перегрузку, имеется три и более версий такой функции с немного отличающимися именами. Так, в С функция its () возвращает абсолютное значение целого числа, функция lacs О — абсолют- ное значение длинного целого числа, а функция fabs() — абсолютное значение числа с плавающей точкой. Поскольку в С перегрузка не поддерживается, каждая функция должна иметь собственное имя, хотя эти функции аналогичны. Такой подход очень усложняет работу: несмотря на то, что основные действия, выполняе- мые каждой функцией, одинаковы, программисту все равно необходимо помнить три имени. В C# подобная ситуация исключена, так как все методы, предназначенные Для вычисления абсолютного значения, могут иметь одно имя. Стандартная библио- тека классов в C# содержит метод Abs о , возвращающий абсолютное значение. Он
Перегрузка метода ZJ 1 перегружается классом System .Math для обработки всех числовых типов. А компилятор, исходя из типа аргумента, определяет, какую версию метода Abs О следует вызвать. Преимущество перегрузки заключается в том, что она позволяет, используя общее имя, получать доступ к методам, выполняющим одинаковые действия с различными Iинами данных. Таким образом, имя .tbs представляет общее выпо.шяемое действие. Выбор необходимой версии метода для указанного типа данных производится ком- пилятором, программист же обязан помнить лишь общее действие, выполняемое данным методом. Благодаря реализации полиморфизма вместо нескольких имен используется только одно. Этот пример с методом АЬс () достаточно простой, но использование перегрузки позволяет решить гораздо более сложные задачи. Вопрос. Мне встречался термин подпись, который используют программисты на С. Что он означает? Ответ. В C# подпись — это имя метола со списком его параметров. Следо- вательно, при перегрузке двух методов одного и того же класса они не могут иметь одинаковую подпись. Обратите внимание, что подпись нс включает тип возвращае- мого значения, поскольку он нс используется компилятором C# для различия перегруженных методов. При перегрузке метода каждая из версий! может выполнятьлюбыс необходимые действия. Пет правила, которое бы определяло, что перегруженные методы должны выполнять одинаковые или похожие действия. Но стиль написания программ в C# предполагает, что перегрузка методов должна быть связана с предыдущей версией метода. Следова- тельно, хотя у вас есть возможность использовать одно имя для перегрузки методов, выполняющих в результате абсолютно разные действия, делать этого не следует. Так, можно использовать имя sqr для создания метода, возвращающею квадрат целочис- ленного значения, и метода, возвращающего квадратный корень значения с плавающей точкой. Но создав два принципиально различных метода, программист только усложнит понимание программы и увеличит вероятность появления ошибок. Поэтому перегружать рекомендуется только тесно связанные операции. Минутный практикум I. Какое условие должно выполняться, чтобы метод мог быть перегружен? 2. Необходимо ли перегруженному методу иметь тип возвращаемого значения, отличающийся от предыдущей версии? 3. Как выполняется автоматическое преобразование типов данных в C# при перегрузке? 1. Для перегрузки метод должен отличаться от своей предыдущей версии по типу и/или количеству параметров. 2. Нет. Тип возвращаемого значения перегруженных методов может отличаться, но эго не является необходимым условием для перегрузки метода. 3. Когда отсутствует прямое совпадение типов между набором аргументов и набором парамет- ров, используется метод с наиболее близко совпадающим набором аргументов, если типы этих аргументов могут быть автоматически преобразованы к типам, имеющим параметры.
232 Глава 6. Детальное рассмотрение методов и классов Перегрузка конструкторов Также как методы, конструкторы могут быть перегружены, что позволяет конструи- ровать объекты различными способами. В качестве примера рассмотрим следующую программу: //В программе демонстрируется использование перегруженного конструктора. using System; class MyClass { public int x; public MyC1ass() { Console. WriteLine ("Этот оператор определен’’ + " в конструкторе MyClass () ."); х = 0; public MyClass(int i) { Console.WriteLine("Этот оператор определен” + " в конструкторе MyClass (int) . ") ; } Конструирование объектов различ- ными способами. public MyClass(double d) { Console.WriteLine("Этот оператор определен” + " в конструкторе MyClass(double)."); х = (int) d; } public MyClass(int i, int j) { Console.WriteLine("Этот оператор определен" + " в конструкторе MyClass(int, int)."); x «- i * j; --------------- } } class OverloadConsDemo { public static void Main() { MyClass tl = new MyClass(); MyClass t2 =*- new MyClass(88); MyClass t3 = new MyClass(17.23); MyClass t4 = new MyClass(2, 4); Console.WriteLine("Значение переменной tl.x: " + tl.x); Console.WriteLine("Значение переменной t2.x: " т t2.x); Console.WriteLine("Значение переменной t3.x: ” + t3.x); Console.WriteLine("Значение переменной t4.x: " + t4.x); } } Результат выполнения программы таков: Этот оператор определен в конструкторе MyClass(). Этот оператор определен в конструкторе MyClass(int). Этот оператор определен в конструкторе MyClass(double).
Перегрузка конструкторов 233 Этот оператор определен в конструкторе MyClassfint, Значение переменной tl.x: О Значение переменной t2.x: 88 Значение переменной t3.x: 17 Значение переменной t4.x: 8 Конструктор MyClass () может быть перегружен четырьмя способами, при исполь- зовании каждого из них объект конструируется по-разному. Выбор компилятором необходимого конструктора осуществляется на основе параметра, указанного при выполнении оператора new. Благодаря перегрузке конструктора появляется возмож- ность гибкого выбора способа конструирования объекта и создания именно такого объекта, который необходим в конкретной ситуации. Использование перегрузки позволяет конструктору одного объекта инициализиро- вать другой объект. Например, рассмотрим программу, в которой используется класс Summation для вычисления суммы всех чисел от ноля до значения, указанного в качестве аргумента конструктора этого класса. // В этой программе один объект используется для инициализации другого. using System; class Summation { public int sum; // Для создания объекта конструктор использует параметр типа int. public Summation(int num) { sum = 0; for (int 1=1; i <- num; i-r+) sum +- i; } // Для инициализации объекта данный конструктор использует другой объект, // переданный ему в качестве параметра. public Summation (Summation ob) { —•—— sum - ob.sum; Один объект используется для инициализации другого объекта. class SumDemo { public static void Main() { Summation si - new Summation(5); Summation s2 = new Summation(s1); Console.WriteLine("Сумма чисел от 0 до 5 (значение переменной si.sum) - " + s 1. s urn) ; Console.WriteLine("Значение переменной s2.sum: " + s2.sum); Результат выполнения программы показан ниже. Сумма чисел от 0 до 5 (значение переменной si.sum) - 15 Значение переменной s2.sum: 15 Как видно из приведенного выше примера, использование одного элемента для создания другого позволяет гораздо эффективнее выполнять инициализацию созда- ваемого объекта. В данном случае при конструировании объекта s2 отсутствует необходимость повторно вычислять сумму всех чисел от Одо 5.
234 Глава 6. Детальное рассмотрение методов и классов Вызов перегруженного конструктора с использованием ключевого слова this При вызове перегруженного конструктора в некоторых случаях бывает полезно использовать вызов одного конструктора другим. В C# это реализуется при помощи еще одной формы ключевого слова this. Общий синтаксис такого конструктора представлен ниже: constructor-name (parameter-list} : this (argument-list} • // ... тело конструктора, который также может быть пустым При вызове этого конструктора (который можно назвать вызывающим) первым выполняется перегруженный конструктор, в котором список параметров совпадает со списком аргументов. Далее выполняются операторы, определенные внутри вызы- вающего конструктора, если таковые имеются. Ниже представлена программа, где используется ключевое слово this. 7/ Б программе демонстрируется использование ключевого слова this для вызова '/ одного конструктора из другого. using System; class XYCoord { public int x, y; public XYCoordO : this(0, 0) { Console . Wri teLine ("Этот оператор определен в конструкторе XYCoord ()’’) ; public XYCoord(XYCoord ob]) : this(003.x, obj.y) { Console.WriteLine("Этот оператор определен в конструкторе ” + "XYCoord (XYCoord obj)"); i public XYCoord(int i, int j) { Console.WriteLine("Этот оператор определен s конструкторе " -» "XYCoord(XYCooro(int, int)"); к - i; У = j; } class Over loadConsDerno { public static void Main() { XYCoord tl - new XYCoord(); XYCoord t2 -= new XYCoord(8, 9); XYCoord t3 - new XYCoord(t2); Console.WriteLine("Значение переменных tl.x и tl.y: " •* tl.x - ", " - tl.y); Console . WriteLine ("Значение переменных t2. x и t2.y: " -r t2. x - ", " + t2.y); Console. WriteLine ("Значение переменных t3.x и Г-З.у: " н 13. х + ", ’’ + t3.y);
Перегрузка конструкторов 235 g результате выполнения этой программы па экран будут выведены следующие строк и: ч,„ оператор определен а конструкторе ,Д. оператор опрелелен а конструкторе o.-r-.'i1 оператор определен в конструкторе Этет оператор определен ь конструкторе Этот оператор определен в конструкторе Значение переменных tl.x и tl.y: О, О Значение переменных t2.x и t2.y: 8, 9 Значение переменных t_3.x и t3.y: 8, 9 XYCoord(XYCoord(int, XYCoord(XYCoord(inc, XYCoord(XYCoord ob j ) Рассмотрим, как работает эта программа. В классе XYCoord единственным конструк- тором. который фактически инициализирует поля х и у, является конструктор XY'. ord (int, int). Остальные два конструктора, используя ключевое слово this, просто вызывают конструктор XYCoord (int, int). Гак, при создании объекта tl вызывается его конструктор XYCoord (), который в свою очередь вызывает конструк- тор :his(0, 0). То есть ключевое слово this указывается вместо полного имени конструктора XYCoord (0, 0). Создание объекта ti происходит таким же образом. Одна из причин использования ключевого слова this для вызова перегруженных конструкторов — возможность избежать дублирования кода. В предыдущем примере без использования ссылки this пришлось бы повторять последовательность инициа- лизирующих операторов (х = i; у = j;) во всех трех конструкторах. Еще одним преимуществом применения ключевого слова this является создание конструкторов с «аргументами но умолчанию», которые применяются, если эти аргументы нс заданы явно. В частности, для предыдущей программы можно создать сше один конструктор XYCcordo: puolic XYCoordfint к): this(к, х) {) который автоматически присваивает координате у то же значение, что и координате х. Учтите, что «аргументы по умолчанию» необходимо использовать осторожно, по- скольку их неправильное определение может привести к ошибкам при выполнении программы. Проект 6-2. Перегрузка конструктора Queue 0 QDemo2.cs этом проекте мы модернизируем класс Queue путем добавления к нему двух конструкторов. С помощью первого конструктора из одной очереди будет создана Другая очередь. Второй конструктор мы используем для создания очереди и присваи- вания начальных значений се элементам (инициализации). Добавление этих двух конструкторов позволит использовать данный класс более эффективно. Пошаговая инструкция Г Создайте новый файл с именем Qbemo2 . ts и скопируйте в него код класса Queue из проекта 6-1. 2- Добавьте в программу представленный ниже конструктор, который создает оче- редь из другой очереди. // Конструктор создает объект типа Queue на основе другого объекта типа Queue. Public Queue(Queue ob) {
236 Г лава 6. Детальное рассмотрение методов и клас putloc = ob.putloc; V getloc - ob.getloc; -Я q - new char[ob.q.Length]; 11 !/ Копирование элементов. J| for(int i-getloc+1; i <~ putloc; H+) q [ 1 ] on. q [ i J ; -!S 1 4 Давайте внимательно рассмотрим этот конструктор. Он присваивает переменным S putloc и getloc начальные значения, содержащиеся в параметре (объекте) оь. 4 Затем он создает новый массив для хранения элементов очереди и копирует их из массива объекта ob в этот массив. После конструирования новая очередь будет !у точной копией начальной (используемой ятя инициализации) очереди, но обе они 1 будут совершенно разными объектами. 3. Добавьте конструктор, который инициализирует очередь из массива символов, в передаваемого в качестве параметра. ;'{ // Конструктор создает объект типа Queue и инициализирует его. public Queue (char [] а) { putloc = 0; getloc = 0; q == new char[a.Length+1]; ‘V %. Л for(int 1 = 0; i < a.Length; i + + ) put(a[i]); 0 1 I Этот конструктор создает очередь, длина которой позволяет хранить все символы из массива а, а затем помещает эти символы в массив очереди. Для корректной работы алгоритма очереди се длина (длина ее массива) должна быть на единицу Д больше длины массива, передаваемого в качестве параметра. 4 4. Ниже представлен полный код обновленного класса Queue вместе с кодом класса £ QDemo2. // Версия класса Queue с двумя дополнительными конструкторами. •'? using System; class Queue { s // Теперь эти члены класса но умолчанию становятся закрытыми. £ char{] q; // Этот символьный массив является основой очереди, int putloc, getloc; // Индексы, указывающие, какому элементу массива // будет присваиваться значение и из какого // элемента значение будет считываться. // Создает пустую очередь по заданному размеру, public Queue (j_nt size) { q = new char[size+1]; // Выделение памяти для очереди, putloc - getloc - 0; } // Конструктор создает объект типа Queue на основе другого объекта типа Queue, public Queue(Queue ob) { putloc = ob.putloc; getloc - ob.getloc; ; q = new char[ob.q.Length]; ?
Перегрузка конструкторов 237 // Копирование элементов. for(int i=getloc+l; i <= putloc; 1++) q [ i ] - ob.q[i]; // Конструктор создает объект типа Queue и инициализирует его. public Queue(char[] а) { putloc - 0; getloc 0; q = new char(a.Length+1] ; for(int i = 0; i < a.Length; i++) put(a[i]); } // Метод помещает символ в очередь. public void put(char ch) { if(putloc==q.Length-1) { Console.WriteLine("- Очередь заполнена."); return; } putloc++; qtputloc] ch; } // Считывает значения элементов массива (очереди). public char get() { if(getloc == putloc) { Console.WriteLine(Очередь пустая.”); return (char) 0; } getloc++; return qtgetloc]; } } // В этом классе демонстрируется использование класса Queue. class QDemo2 { public static void Main() { // Создается пустая очередь из десяти элементов. Queue ql = new Queue (10); char[] name - 'o’, ’m’}; // Создается очередь на основе массива. Queue q2 = new Queue(name); char ch; int i; // В очередь помещаются некоторые символы. for(i=0; i < 10; i++) ql.put( (char) (’A' + i) ) ; // Создается очередь на основе другой очереди. Queue q3 - new Queue(ql); // Отображение значений элементов очереди.
^LOO Г лава 6. Детальное рассмотрение методов и классов Console.Write("Содержимое очереди ql: "); for(i-0; i < 10; i++) { ch - ql.get(); Console.Write(ch); Console . WrircL^iie ("\n") ; Console.Wrice("Содержимое очереди qz: "); for(i-0; i < 3; i*+) { ch - q2 .get () ; Console.Write(ch); Console.WriteLine("Хп”); Console.Write("Содержимое очереди оЗ: "); for(r-0; i < 10; i+ч ) { ch - q3.get(); Console.Write(ch); 5. Ниже представлен результат выполнения программы. Содержимое очереди ql: ABCDEFGHIJ Содержимое очереди q2: Tom Содержимое очереди q3: ABCDEFGHIJ Метод Main() До настоящего момента мы использовали только одну версию метода Main (). Но в C# существует несколько перегруженных версий этого метода. Некоторые из них могут использоваться для возвращения значений, а другие могут принимать аргумен- ты. Каждая из этих версий будет рассматриваться в данном разделе. Возвращение значений методом Main() При завершении работы программы метод Main О может возвратить значение вызывающему процессу (часто операционной системе). Для этого используется следующий синтаксис метода Mair. (): public static int MainO Обратите внимание, что вместо типа возвращаемого значения void в этой версии метода Main () указан тип int. Следовательно, возвращаемое этим методом значение будет иметь тип int. Обычно возвращаемое значение метода Mair. () указывает способ завершения про- граммы. По соглашению, если возвращается значение 0, то программа завершена корректно. Все другие значения указывают на ошибку, возникшую при выполнении программы.
Метод MainQ 239 Передача аргументов методу Main() Многие программы принимают так называемые аргументы командной строки. Аргу- мент командной строки — это информация, которая следует непосредственно за именем программы в командной строке. В Сопрограммах такие аргументы переда- ются методу Main (). Чтобы этот метод мог принимать аргументы, необходимо использовать следующий синтаксис: public static void Main(string[] args) или pun-ic static int Main(string[J args) В первом варианте метод Main() имеет гип возвращаемого значения void; второй вариант можно использовать для возвращения целочисленного значения, как уже описывалось в предыдущем разделе. В обоих случаях аргументы командной строки хранятся в виде строк в массиве типа string, который передастся методу Main (). Например, в следующей программе на экран выводятся все аргументы, с которыми эта программа вызывается. // Программа отображает все аргументы, заданные в командной строке, using System; class CLDemo { puolic static void Main(string[] args) { Console. Wri teLine (’’Программе были переданы ’’ -r args. Leng ch +• ” аргумента командной строки.'*); Console.WriteLine("Список аргументов: *’) ; for (int i=0; Kargs . Length; i++) Console.WriteLine(args[i]); Если в командной строке ввести имя программы CLDemo и задать три аргумента CLDemo один, два, три. на экран будут выведены следующие строки: Поограмме были переданы 3 аргумента командной строки. Список аргументов: а дин, дна, гри. Чтобы понять механизм использования аргументов командной строки, рассмотрим следующую программу. Она принимает один аргумент командной строки - имя пользователя. Затем производится поиск этого имени в двухмерном строковом массиве. Если такое имя найдено, на экран выводится телефонный номер этого пользователя. 1Простой автоматизированный телефонный справочник, using System; class Phone { public static int Main(string[] args) { string[,] numbers = { { "Владимир", "555-3322" }, ( "Людмила", "555-8976" },
Глава 6. Детальное рассмотрение методов и классов { "Сергей", "555-1037" }, { "Александра"г "555-1400" }, { " ", "" } // Определена нолевая строка для случая, когда не будет // введено никакого имени. / ; int i; if(args.Length 1) { Console.WriteLine("Шаблон ввода: Phone <имя>"); return 1; // Значение, указывающее на неправильный ввод аргумента и // преждевременное завершение программы. I else ( for(i-0; numbersfi, 0] !* 1+*) { if(numbers[ir 0] args(0]) { Console.WriteLine(numbers[i, 0] + ": " + numbers[i, 1j) ; break; } } if(numbers[i, 0] == "”) Console.WriteLine("Имя не найдено."); } return 0; } } Ниже приведен пример работы этой программы. D:\Work\С#\programs\ch_C6>Phone Людмила Людмила: 555-8976 Рассмотрим данную программу подробнее. Во-первых, заметьте, что перед продол- жением выполнения программы проверяется наличие аргумента командной строки. Это очень важный момент. Если корректная работа программы зависит от количества введенных аргументов, всегда необходимо вначале проверить количество аргументов командной строки, передаваемое такой программе. Невыполнение этого условия часто приводит к аварийному завершению работы программы. Во-вторых, обратите внимание на то, как программа возвращает код завершения работы. Если программе не было передано требуемое количество аргументов, то возвращается значение 1, что указывает на аварийное завершение программы. Если программе было передано нужное число аргументов, то при завершении программы возвращается значение 0. Минутный практикум i 1. Может ли конструктор принять в качестве параметра объект своего класса? Для чего применяются перегруженные конструкторы? 3. Покажите синтаксис метода Main(), который принимает аргументы ко- мандной строки, но нс возвращает значение. 1. Да. 2. Перегруженные конструкторы применяются для обеспечения удобства и гибкости при использовании классов. 3. public static void Main(string[] args)
рекурсия 241 рекурсия В С" метод может вызывать сам себя. Этот процесс называется рекурсией, а метод, который сам себя вызывает, называется рекурсивным. Ключевым компонентом рекурсивного метода является оператор для вызова этого же метода. Классическим примером рекурсии является вычисление факториала числа. Факто- риалом числа N называется произведение всех целых чисел от I до N (скажем, факториал числа 3 равен произведению 1*2*3 = 6). В следующей программе представлен рекурсивный метод, предназначенный для вычисления факториала числа. Для сравнения в программу включен равнозначный метод, выполняющий тс же функции без использования рекурсии. В программе демонстрируется использование рекурсии для вычисления факториала числа. using System; class Factorial { // Рекурсивный метод. public int factR(int n) { int result; if(n==l) return 1; result - factR(n-l) * n; ◄- return result; Рекурсивный вызов метода f actR (). 11 Равнозначный метод, public int factl (int n int t, result; в котором вместо рекурсии используется цикл for. { result - 1; for(t=l; t <=-- n; t + + ) result ж= t; return result; class Recursion { public static void Main() { Factorial f = new Factorial(); Console.WriteLine("Факториал числа, " метода."); Console.WriteLine("Факториал числа Console.WriteLine("Факториал числа Console.WriteLine("Факториал числа Console.WriteLine(); вычисленный с помощью рекурсивного" + 3 = " + f.factR(3) ) ; 4 = " + f.factR(4)); 5 = " •+ f. factR (5) ) ; Console.WriteLine("Факториал числа, "метода."); Console.WriteLine("Факториал числа Console.WriteLine("Факториал числа Console.WriteLine("Факториал числа вычисленный с помощью обычного 3 = " + f.factl (3)); 4 = ” + f.factl (4)); 5 = " + f.factl(5));
242 Глава 6. Детальное рассмотрение методов и класс! Ниже показан результат выполнения этой программы. w Факториал числа, вычисленный с помощью рекурсивного метода- W| Факториал числа 3-6 Факториал числа 4 -24 Ж Факториал числа 5 - 12 С ’’ffi Факториал числа, вычисленный с помощью обычного метода. Факториал числа 3=6 Ф Факториал числа 4 ~ 24 'иЖ Факториал числа 5 ~ 120 Алгоритм работы обычного метода fact() достаточно прост. В нем используетоИ* цикл, где последовательно умножаются все целые числа от 1 до числа, факториадЯ которого нужно вычислить. Я* Работа рекурсивного метода factR() немного сложнее. Если вызвать метод factR() с» аргументом 1, метод возвращает единицу, которая при возврате управления предыдуще-Я му методу factR () будет использоваться в выражении factR (п-1 j При выполнении» этого выражения метод factR () вызывается с аргументом п~1. Этот процесс повторяетсЯ до тех пор, пока значение переменной п не будет равно 1. Так, при вычисленикД факториала числа 2 первый! вызов метода factR () с аргументом 2 приведет ко втором Л вызову этого же метода, но уже с аргументом 1. При получении этого аргумента мето.Д возвратит число 1, которое затем будет умножено на 2. То есть ответ — значение 2. Можно® поместить оператор WriteLine (); в метод factR (), который будет выводить на экран;», аргумент каждого вызова и промежуточные результаты. Я Когда метод вызывает сам себя, в стек помещаются новые локальные переменные и» параметры, а код метода выполняется с этими новыми переменными с самого начала.» Рекурсивные вызовы не создают новую копию метода, новыми являются только» аргументы. После возвращения рекурсивным методом значения старые локальные® переменные и параметры удаляются из стека, а управление программой передается» оператору, следующему за тем, из которого был в вызван метод. Ж Рекурсивные версии многих программ могут выполняться медленнее своих итеративных* (использующих обычные методы и циклы) аналогов, так как при дополнительных ® вызовах метода появляются непроизводительные издержки. В результате слишком:® большого количества рекурсивных вызовов создается много новых локальных перемен- > ных и параметров, а это может перегрузить стек, что приведет к возникновению j, исключительной ситуации. Обычно это происходит, если в программе неправильно £ определено условие: метод вместо вызова самого себя должен вернуть значение. 4 Основное преимущество рекурсии заключается в том, что некоторые алгоритмы рекурсивно могут быть реализованы проще и эффективнее, чем их итеративный вариант (например, алгоритм быстрой сортировки достаточно сложен при его итера- тивной реализации). Рекурсивные решения требуются и при решении некоторых задач, связанных с разработкой искусственного интеллекта. Как правило, для возврата из метода значения без выполнения рекурсивного вызова используется условный оператор, такой как if. Учтите, что при неправильном определении условного выражения после вызова метод будет бесконечно вызывать сам себя. У начинающих программистов такая ошибка при использовании рекурсии встречается достаточно часто. Мы рекомендуем для контроля над выполнением программы как можно чаше использовать операторы WriteLine о ; и прерывать <; программу в случае обнаружения ошибки. i
f ключевое слово static 243 [(дючевое слово static Иногда требуется определить член класса, который будет использоваться независимо оТ побых объектов этого класса. Обычно доступ к члену класса осуществляется с 11спо.|ьзованием объекта этого класса, но существует возможность создать член к Kicca, который может применяться самостоятельно без ссылки на созданный объект какого-либо класса. При объявлении такого члена класса используется ключевое сюно static. Если член класса определяется с модификатором static, он стано- вится доступным до создания объектов этого класса и без ссылки на какой-либо объект. Ключевое слово st atic может использоваться для объявления как методов, так и переменных. Наиболее известным членом класса с модификатором static является метод Main (), который объявляется как static, поскольку должен вызы- ваться операционной системой при запуске программы. Для использования члена класса с модификатором static за пределами класса следует указать имя его класса, за которым следует оператор точка (.). В этом случае создавать объект нет необходимости. Фактически доступ к члену класса с модифи- катором static нельзя осуществить, указав идентификатор объекта, доступ к нему осуществляется только с использованием имени класса. Например, если переменной static count, являющейся частью класса Timer, необходимо присвоить значение 10, нужно использовать следующий оператор: Timer.count ~ 10; Этот синтаксис аналогичен синтаксису, используемому для доступа к обычной переменной экземпляра, за исключением того, что вместо идентификатора объекта применяется имя класса. Метод с модификатором static также вызывается с использованием имени его класса и оператора точка. Переменная, объявленная с модификатором static, фактически является глобаль- ной переменной. При объявлении объектов ее класса не создается копия такой переменной, вместо этого все объекты класса совместно используют одну перемен- ную с модификатором static, которая инициализируется при загрузке ее класса. Если начальное значение нс указано явно, по умолчанию такой переменной при- сваивается значение 0 (если это переменная числового типа), значение null (если но переменная ссылочного типа) и значение false (если это переменная типа bool). Гакпм образом, переменная с модификатором static всегда имеет значение. Различие между методом с модификатором static и обычным методом состоит в том, ’Но методе этим модификатором может быть вызван с использованием имени его класса бс 1 создания какого-либо объекта этого метода. Ранее нам уже встречался пример такого «ызова — метод Sqrt О с модификатором static класса System.Math. Ниже представлена программа, в которой создаются переменная и метод с модифи- каторами static. В программе демонстрируется использование модификатора static. System; c-ass StaticDemo { / Объявление статической переменной, public static int val = 100; It Объявление статического метода. I
244 Глава 6. Детальное рассмотрение методов и классов public static int valDiv2() [ return val/2; j } class SDemo { public static void Main() ( Console.WriteLine ("Первоначальное значение переменной StaticDemo.val = •' + StaticDemo.val); StaticDemo.val = 8; Console.WriteLine("Значение переменной StaticDemo.val = " + StaticDemo.val); Console.WriteLine("Значение этой же переменной после выполнения метода " + "StaticDemo.valDiv2 () = " + StaticDemo.valDiv2 ()) ; } } В результате работы этой программы будут выведены следующие строки и значения: Первоначальное значение переменной StaticDemo.val = 100 Значение переменной StaticDemo.val = 8 Значение этой же переменной после выполнения метода StaticDemo.valDiv2() = 4 Как видно из результата работы программы, переменная с модификатором static инициализируется в начале работы программы до создания какого-либо объекта ее класса. Методы с модификатором static обладают некоторыми ограничениями: О метод с модификатором static не имеет ссылки this; О метод, объявленный как static, может непосредственно вызвать только другой статический метод (метод с модификатором static). Он не может вызвать метод экземпляра своего класса, потому что методы экземпляра класса могут работать с данными экземпляров этого класса, а статические методы не могут; О метод с модификатором static имеет прямой доступ только к статическим данным, он не может использовать переменную экземпляра, поскольку не работает с экземплярами своего класса. Например, в приведенном ниже фрагменте метод valDivDenom() с модификатором static является недействительным: class StaticError ( int denom =3; // Обычная переменная экземпляра. static int val = 1024; // Статическая переменная. /* Ошибка! Статический метод не может непосредственно обращаться к обычной переменной. */ static int valDivDenom() { return val/denom; // Этот оператор не будет скомпилирован. } ) Обычная переменная экземпляра, доступ к которой не может быть осуществлен из метода с модификатором static, указывается с помощью параметра denom. Однако использование переменной val разрешено, поскольку это переменная статическая.
Ключевое слово static 245 В следующей программе та же проблема возникает при попытке вызова обычною меюда nonStaticMeth (}, определенного в классе AnotherStat г cEt r<. г, из метода it.icMeth (), объявленного как static, и тоже определенного в этом классе. using System; c_ciss AnotherStaticError { '! Обычный метод. void nonStaticMeth() { Console.WriteLine("Оператор метода nonStaticMethО.”); /* Ошибка! Статический метод не может непосредственно обращаться к обычному. static void staticMethO { nonStaticMeth(); // Этот оператор не будет скомпилирован. i В этом случае попытке! вызова обычного метода (то есть метода экземпляра) из метода с модификатором static приводит к ошибке компилирования программы. Обратите внимание, что метод е модификатором static может вызывать методы экземпляра и получать доступ к переменным экземпляра своего класса, но осущест- влять это необходимо, используя объект данного класса (то есть для доступа к методу или переменной экземпляра нужно указывать название объекта). Приведенный ниже фрагмент кода является действительным. c_.ass MyClass { // Обычный метод. void nonStaticMeth() { Console.WriteLine("Оператор метода nonStaticMeth()."); ' * Используя переменную ссылочного типа, можно вызнать обычный метод из статического метода. V public static void staticMeth (MyClass ob) { ob.nonStaticMeth(); // Это действительный вызов метода. Минутный практикум * 1. Дайте определение рекурсивного метода. 'JS 2. Объясните различие между переменными с модификаторами static и переменными экземпляра. 3. Может ли метод с модификатором static вызывать обычный метод одного с ним класса без использования ссылки на объект. I - Рекурсивным называется метод, который вызывает сам себя. 2 Каждый объект класса имеет свои собственные копии переменных экземпляра, определен ные классом. Все объекты класса совместно используют одну копию переменной с моди- фикатором static. 2. Нет.
246 Глава 6. Детальное рассмотрение методов и классов Проект 6-3. Алгоритм Quicksort QSDemo.cs В главе 5 был представлен простой алгоритм сортировки, называемый пузырьковым а также было сказано, что существуют более эффективные методы сортировки. В этом проекте мы разработаем версию одного из лучших способов сортировки Quicksort (алгоритм быстрой сортировки). Этот алгоритм не описывался в главе 5, поскольку его работа основана на рекурсии. Версия, которую мы будем разрабатывать, предна- значена для сортировки символьного массива, но принцип работы этого алгоритма может быть применен для сортировки объектов любого типа. Принцип работы алгоритма быстрой сортировки состоит в следующем — большой объем данных делится на более мелкие части, которые успешно и быстро обрабаты- ваются. Поскольку алгоритм Quicksort применяется для сортировки элементов мас- сива, то логично при делении этого объема данных на части использовать значение длины массива. Из значения крайнего правого индекса вычитается значение крайнего левого индекса и делится на 2. Полученный результат является индексом элемента (в дальнейшем мы будем называть его средней точкой), значение которого становится ориентиром на первом этапе сортировки. Все элементы, имеющие значения большие или равные значению средней точки, перемещаются в правую часть массива, а элементы, имеющие значения меньшие, чем значение срсдысй точки, перемешаются в левую часть массива. То есть в результате выполнения первого этапа сортировки массив как бы делится на две части. Затем этот же процесс повторяется с каждой частью до тех пор, пока весь массив не будет отсортирован. Например, если в качестве средней точки используется элемент со значением d, то после первого этапа сорти- ровки элементы символьного массива f е d а с Ь будут перераспределены следующим образом: Первоначально f е d а с Ъ После первого этапа сортировки b с a d е f Затем этот процесс повторяется для каждой части массива, то есть для ь с a nd е f. Выбрать среднюю точку можно двумя способами — произвольной путем нахождения среднего значения небольшой группы значений, взятых из массива. Для оптимальной сортировки необходимо выбрать значение, находящееся в середине диапазона зна- чений элементов массива, но для многих наборов данных это осуществить не очень просто. В худшем случае выбранное значение будет одним из крайних значений (либо минимальным, либо максимальным). Но даже в этом случае алгоритм Quicksort будет работать корректно. В разрабатываемой нами версии программы в качестве средней точки выбирается средний элемент массива. Пошаговая инструкция 1. Создайте файл и назовите его QSOemo.cs. 2. Создайте класс Quicksort, как показано ниже. // Простая версия алгоритма Quicksort. using System; class Quicksort {
ключевое слово static 247 // Метод предназначен для вызова другого метода gs, а котором // непосредственно реализован алгоритм Quicksort. public static void qsort(char(] items) { qs(items, 0, items.Length-1) ; } •' Рекурсивная версия алгоритма Quicksort, предназначенная для сортировки // символьного массива. static void qs(charr’ items, int left, int right) int i, 3; chai x, y; i left; j = right; x - items[(Left+rignr)/2]; do { while( (items[i] < x) && while((x < items[j]) && (i < right)) i++; (j > left)) j —; if(i <-= j) J у - items f ij; i ternsfi] - iterns Ij j; itemsfj] y; i++; j-~; } } while (i <~ j ) ; if (left < j) qs(items, Left, j); if(i < right) qs(items, i, right); Для упрощения использования класса Quicksort в нем объявлен метод qsort О , предназначенный для определения параметров и вызова метода qs(), который непосредственно выполняет сортировку элементов. Это позволяет вызывать метод jsorto, указывая только имя массива, который необходимо отсортировать. Поскольку метод qs() используется только внутри класса, по умолчанию он определяется как private. Ч гобы использовать класс Quicksort, нужно вызвать метод Quicksort . qsort () . 1(оскольку метод qsort () определен как static, он может вызываться с исполь- зованием имени его класса, а не объекта, следовательно, нет необходимости создавать объект типа Quicksort. После возвращения методом значения массив будет отсортирован. Помните, что эта версия программы предназначена для сортировки только символьного массива, но принцип работы алгоритма можно использовать для сортировки массива любого типа. 4. Ниже представлена программа, демонстрирующая работу алгоритма Quicksort. Простая версия алгоритма Quicksort. // Метод предназначен для вызова другого метода gs, в котором
248 Глава 6. Детальное рассмотрение методов и классе // непосредственно реализован алгоритм Quicksort. public static void qsort(char[] items) { qs(items, 0, items.Length-1); } // Рекурсивная версия алгоритма Quicksort, предназначенная для сортировкр II символьного массива. static void qs(char[] items, int left, int right) { int i, j; char x, y; i = left; j - right; x = items[(left+right)/2]; do { while((items[i] < x) && (i < right)) i++; while((x < items[j]) && (j > left)) j--; if(i <- j) { у — items[ i ] ; items[i] = items[j]; items[j] у; i++; j —; } } while(i <- j); if(left < j) qs(items, left, j); if (i < right) qs(items, i, right); class QSDemo { public static void Main() { char[] a = { ’d’, 'x’, ’a’, 'r', 'p', ’j’, 'i' }; int i; Console.Write("Первоначальный массив: ”); for(i^0; i < a.Length; i-r+) Console.Write(a [i] ) ; Console.WriteLine() ; Il Выполняется сортировка массива. Quicksort.qsort(a); Console .Write ("Отсортированный массив: ’’); for(i=0; i < a.Length; i++) Console.Write(a[i]) ;
Контрольные вопросы 249 ^Контрольные вопросы 1. Используя фрагмент кола class X { int count; определите, является ли корректным следующий код: class Y { public static void Main() { X ob = new X () ; ob.count = 10; 2. Где должен располагаться модификатор при объявлении члена класса? 3. В стеке используется принцип LIFO (last in, first out - «последним зашел, первым вышел»). Стек часто сравнивается со стопкой тарелок — тарелка, первой поставленная на стол, будет использована последней. Создайте стек с именем Stack для хранения символов. Методы, осуще- ствляющие доступ к стеку, назовите push() и рор(). Необходимо дать пользователю возможность указывать размер стека при его создании. Все остальные члены класса Stack должны иметь закрытый доступ (private). Подсказка: в качестве модели используйте класс Queue, в котором необ- ходимо только изменить способ доступа к данным. 4. Задан следующий класс: class Test { int а; Test(int i) { a = i; } } Напишите метод (swap ()), меняющий местами содержимое двух объектов типа Test, на которые ссылаются две переменные ссылочного типа. 5. Является ли корректным приведенный ниже фрагмент кода? class X { int meth(int a, int b) { ... } string meth(int a, int b) { ... } 6. Напишите рекурсивный метод, предназначенный для вывода строки в обратном порядке. 7. Как объявлять переменную, которую должны совместно использовать все объекты класса? 8. Какие функции выполняют модификаторы параметров ref и out? Чем они отличаются? 9. Напишите четыре варианта синтаксиса метода Main о . 10. Какие вызовы будут действительными для заданного фрагмента кода? void meth(int i. int j, params int [] args) l И a) meth (10, 12, 19) 6) meth(10, 12, 19, 100) в) meth (10, r) meth (10, 12, 12) 19, 100, 200) t s f I
глава л Перегрузка операто индексаторы и свойства □ Основные сведения о перегрузке оператора □ Перегрузка бинарных операторов □ Перегрузка унарных операторов □ Перегрузка операторов сравнения □ Применение индексаторов □ Создание свойств
ререгРУзка опеРат°Р°в 251 g гпавс рассматриваются три специальных типа членов класса: перегружаемые операторы, индексаторы и свойства. С их помощью можно создавать типы классов, аналогичные встроенным типам. Перегрузка операторов д С- можно определять действия, выполняемые оператором по отношению к какомх-нибудь классу, который вы создаете. Это называется перегрузкой и.т переоп- редеаснием оператора. Функции оператора можно полностью контролировать, но следтег учитывать, что они различаются в зависимости от того, к какому классу применяется оператор. Например, в классе, определяющем связный список, опера- тор + используется для добавления объекта к списку, в классе, реализующем стек, этот оператор применяется для помещения объекта в стек, в других классах он может выполнять иные функции. При выполнении перегрузки исходное предназначение оператора не теряется. Просто к его арсеналу добавляются новые операции, которые будут применяться по отно- шению к определенному классу. Поэтому, например, перегрузка оператора + для обработки связного списка нс приведет к потере его исходного предназначения — выполнению операции сложения целых чисел. Основным преимуществом использования перегрузки оператора является то, что при этом в программную среду можно легко интегрировать новый тип класса. Если для класса определены операторы, то действия с объектами этого класса можно выпол- нять, используя обычный синтаксис (то есть вместо применения отдельного метода, предназначенного для работы с переменными этого класса, достаточно ввести, например, такую строку кода: объект -1- объект;). Кроме того, объекты можно использовать в выражениях, содержащих данные других типов. Процесс перегрузки операторов очень похож на процесс перегрузки метода. Пере- гружая оператор, вы используете ключевое слово operator, определяющее метод оператора, который задает его действие. Синтаксис метода оператора В основном в C# используются два вида операторов — унарные и бинарные (опера- торы, для которых указываются два операнда). Для их перегрузки применяют два вида методов оператора, синтаксис которых представлен ниже. '-китаксис метода, используемого для перегрузки унарного оператора. Pub.j-c static ret-type operator op {param-type operand) операции // c , интаксис метода, используемого для перегрузки бинарного оператора. --с static ret-type operator op (param-typel operand!, param-type! °Perand2) // ... , операции
252 Глава 7. Перегрузка оператора, индексаторы и свойств Перегружаемый оператор (например, г или /) подставляется вместо слова ор Вместо словосочетания ret-type указывается тип значения, возвращаемого пр, выполнении данного метода. Эго значение может принадлежать к любому выбран ному пользователем типу, но обычно оно имеет тип класса, для которого выполняете перегрузка оператора. Поэтому перегружаемые операторы могут использоваться выражениях. Методам, переопределяющим унарные операторы, операнды передаю] ся с помощью параметра operand, а переопределяющим бинарные операторы, — помощью параметров operandl и operand?. i Для перегрузки унарных операторов операнд должен принадлежать к типу класс! для которого определяется оператор. Для перегрузки бинарных операторов тип кд минимум одного из операндов должен совпадать с типом класса, для которец переопределяется оператор. Таким образом, в C# невозможно перегружать оператор дтя тех объектов, которые были созданы не вами. Например, нельзя переопредели! оператор + для типа int или string. Кроме того, параметры оператора не лолжц включать модификаторы ref или out. Перегрузка бинарных операторов Чтобы продемонстрировать использование описанного выше процесса перегрузи оператора, рассмотрим программу, в которой перегружаются два бинарных оператор + и -. Вначале создастся класс ThreeD, содержащий координаты объекта в трехме( ном пространстве. Перегруженный оператор + выполняет сложение соответствуюпо координат двух объектов класса ThreeD. Перегруженный оператор - вычитает коо] динаты одного объекта из соответствующих координат другого объекта. ; //В программе демонстрируется использование перегруженных операторов. < using System; ( // В классе содержатся три переменные x,y,z - координаты объекта. j class ThreeD { int x, у, z; // Координаты объекта. i public ThreeD( ) {x = у = z = 0; } public ThreeD(int i, int j, int k) { x = i; у - j; z = k; } i // Перег-рузка бинарною оператора +. public static ThreeD operator +(ThreeD opl, ThreeD op2) ', t _____________________________________- ThreeD result - new ThreeDO; L—-j Перегрузка оператора + для объектов типа Th гее С /* Сложение значений двух соответствующих координат и возвращение Ж результата.*/ > result.х — opl.x + ор2.х; // В этих трех строках кода выполняется result.у - opl. у + ор2.у; // операция сложения целых чисел, | result. 2 - opl. z +- op2.z; // для которых оператор + сохраняет t // свое исходное предназначение. 1 return result; ж } ------------------------------------------HI Перегрузка оператора - для объектов типа Threevj // Перегрузка бинарного оператора -. 1 public static ThreeD operator -(ThreeD opl, ThreeD op2) 1
рярегрУэка операторов 253 ThreeD result = new ThreeDO; /* Обратите внимание на порядок следования операндов — opl является левым операндом, а ор2 — правым. */ result-х - opl.x - ор2.х; // Оператор - применяется к значениям, result.у = opl.y - ор2.у; // имеющим тип int. result.z == opl.z - op2.z; return result; i // Вывод значений координат X, Y, Z. public void show() t Console.WriteLine(x т ", " + у + ", ” + z) ; class ThreeDDemo { public static void Main() { ThreeD a - new ThreeD(1, 2, 3); ThreeD b new ThreeD(10, 10, 10); ThreeD c = new ThreeDO; Console.Write("Координаты объекта a: ") ; a.show( ); Console. WriteLine(); Console. Write("Координаты объекта b: ”); b. show( ); Console.WriteLine(); Применение в выражениях объектов класса ThreeD, а также операторов + и с - а + Ь; // Операция сложения объектов а и Ь. -ч-—•—--------— Console.Write("Результат операции сложения объектов а и Ь: "); с.show () ; Console.WriteLine(); c-a+b+c; // Операция сложения объектов а, b и с. -<———--------------— Console.Write("Результат операции сложения объектов а, b и с: "); •- - show () ; Console .WriteLine. () ; с ~ с - а; // Вычитание объекта а из объекта с. <——-—————------ Console.Write("Результат выполнения операции вычитания с - а: "); с.show( ); Console.WriteLine О; с - с - b; // Вычитание объекта b из объекта с.-<--------------—-— console.Write("Результат выполнения операции вычитания с ~ Ь: "); с.show( ); Console.WriteLine();
254 Глава 7. Перегрузка оператора, индексаторы и ceoi Результат выполнения программы: Координаты объекта а: 1, 2, 3 Координаты объекта Ь: 10, 10, 10 Результат операции сложения объектов а и Ь: 11, 12, 13 Результат операции сложения объектов а, Ъ и с: 22, 24, 26 Результат выполнения операции вычитания с - а: 21, 22, 23 Результат выполнения операции вычитания с - Ъ: 11, 12, 13 Проанализируем текст программы с момента перегрузки оператора -г. К двум об: там типа ThreeD применяется оператор +, в результате чего значения соответств щих координат складываются так, как определено в методе operator + (). Ода использование данного метода не приводит к обновлению значений перемен] каждого операнда. После выполнения метода возвращается новый объект т ThreeD, содержащий результат выполнения операции. Чтобы понять, почему отк ция сложения нс изменяет содержимое объекта, представьте действие стандарт арифметической операции сложения, например, в выражении К) + 12. Результа выполнения операции будет число 22, но ни число 10. ни число 12 не изменяют результате ее применения. Хотя нс существует правила, запрещающего опера: после перегрузки изменять значения одного из операндов, желательно, чтобы п< перегрузки оператор сохранял свое первоначальное предназначение. Как говорилось ранее, метод operator + О возвращает объект типа ThreeD. > метод может возвращать значение любого допустимого в C# типа, то, что опер; возвращает объект типа ThreeD, позволяет применять этот оператор is выражен состоящих из нескольких операндов, например, а+Ь+с. Здесь, в результате вычи ния выражения а+Ь возвращается результат, принадлежащий типу ThreeD. За этот результат (объект) прибавляется к объекту с. Если же в результате выполне! выражения а+Ь будет возвращено значение, принадлежащее иному типу, подоб выражение будет недействительным. Внутри метода operator + о сложение значений отдельных координат сводите сложению целых чисел. Отдельные координаты х, у и z представлены значения; принадлежащими целочисленному типу, поэтому перегрузка оператора + для обь тов класса ThreeD нс окажет влияния на действия, производимые этим onepaTOf над с целыми числами. Рассмотрим метол operator - (). Оператор - функционирует так же. как оператор но при его использовании важен порядок расположения параметров. Напомним, сложение коммутативно, а вычитание нет (то есть выражение .1 — В нс тождестве выражению В — >1). Для всех бинарных операторов первый параметр в методе opera является левым операндом, второй параметр — правым. При перегрузке некоммута ных операторов необходимо учитывать взаимное расположение операндов. Перегрузка унарных операторов Перегрузка унарных операторов реализуется аналогично перегрузке бинарных раторов, но при этом используется только один операнд. В следующем при рассматривается метод, перегружающий в классе ThreeD оператор унарного мШ // Перегрузка унарного public static ThreeD operator - ( ThreeD op)
255 ререгрУ^ операторов ThroeD result re s u ;- result new ThreeDO; x - -op.x у -op.у z - -op.z J К значениям объекта, переданного в качестве параметра методу operator -(), применяется оператор унарный минус (то сеть положительные значения становятся отрицательными). Полученные значения присваиваются переменным созданного объекта result, затем этот объект возвращается оператором return; в качестве результата. Обратите внимание, что операнд остался неизменным. Это не противоре- чит обычному предназначению унарного минуса. Например, в выражении а = -ь операнду т присваивается значение операнда Ь со знаком минус, но значение операнда при этом не изменяется. Однако в двух случаях (при использовании операторов инкремента и декремента) в методе operator должно быть изменено значение операнда. Поскольку обычным предназначением операторов ++ и — является увеличение или уменьшение соответ- ственно значения операнда на единицу, то перегруженные версии названных опера- торов тоже должны выполнять эти действия. То есть при перегрузке оператора инкремента или декремента операнд должен быть изменен. В качестве примера рассмотрим переопределение метола operator ++() для класса ThreeD. // Перегрузка унарного оператора ++. public static ThreeD operator ++(ThreeD op) ( // Выполняется изменение аргумента. Ор.х+г; ор. У—; < Op.ZTl; Оператор ++ изменяет свой операнд. return op; // Возвращается измененный операнд. Ниже приводится расширенная версия предыдущей программы, демонстрирующая использование унарных операторов - и ++. / / в программе демонстрируется использование перегруженных операторов. Usirg System; // Кг - n 1асс/ содержащий значения координат объекта в трехмерном пространстве. ThreeD ( ini- у, г; I; Координаты объекта. №1дс ThreeDO { х - у - z - 0; ; 7hreeD(int i, int j, int k) { x - i; у j; z k; } // ~ "^Рсгрузка бинарного оператора +. L-i-c static ThreeD operator (ThreeD opl, ThreeD op2) ThreeD result new ThreeDO;
256 Глава 7. Перегрузка оператора, индексаторы и свой< /* Сложение значений соответствующих координат двух объектов и возврат результата - объекта result. х/ result.х - opl.x +- ор2.х; // В этих трех сороках кода выполняется result, у - opl. у » ор2.у; // сложение целых чисел, для которых result.z - opl.z • op2.z; // оператор - сохраняет свое исходное // предназначение. return result; i // Перегрузка бинарного оператора public static ThreeD operator -(ThreeD opl, ThreeD op2) ThreeD result = new ThreeDQ; /* Обратите внимание на порядок следования операторов. Операнд opl находится слева, а ор2 справа. */ result.х - opl.x - ор2.х; // Из одного целочисленного значения result.у = opl.у - ор2.у; // вычитается другое целочисленное // значение. result.z opl.z - op2.z; return result; // Перегрузка унарного оператора public static ThreeD operator -(ThreeD op) ThreeD result - new ThreeDO; result.x = -op.x; result.у = -op.у; result, z == -op.z; return result; // Перегрузка унарного оператора -г-г. public static ThreeD operator н4- (ThreeD op) { // Выполняется изменение аргумента. op.хт+; op.y-r; ор. z + -t; return op; '/ Вывод значений координат X, Y, Z. pub-ic void show() Console .WriteLine (x + ", ” + у + ", ’’ +- z) ; class ThreeDDemo {
реРегРУзка опеРат°Р°в 257 public static void MainO ( ThreeD a new ThreeD(1, 2, 3); ThreeD b = new ThreeD(10, 10, 10); ThreeD c - new ThreeDO; Console.Write("Координаты объекта a: "); a - show(); vnsole.WriteLine() ; Console.Write("Координаты объекта b: "); о.snow (); Console.WriteLine(); г - a + b; // Операция сложения объектов а и b. Console .Write ("Результат операции сложения объектов а и b: с.show (); Console.WriteLine(); с -= а + b + с; // Операция сложения объектов а, b и с Console. Write ("Результат операции сложения объектов а, b и с: ") ; z. show () ; Console.WriteLine(); с ~ с - a; // Объект а вычитается из объекта b. Console.Write("Результат выполнения операции вычитания с - а: "); с.show(); Console.WriteLine() ; с - с - b; // Объект b вычитается из объекта с. Console.Write("Результат выполнения операции вычитания с - Ь: "); с.show (); Console.WriteLine(); с =* -а; // Объекту с присваивается значение объекта а, // взятое со знаком минус. (Переменным объекта с присваиваются // значения объекта а, взятые со знаком минус.) Console.Write("Результат выполнения операции -а: "); с.show(); Console.WriteLine() ; а + + ; // Выполнение перегруженного оператора-г-г Console.Write("Результат выполнения операции а++: "); а.show(); з ул ьтат вы пол нения п ро гра .м м ы: -' -рдинаты объекта а: 1, 2, 3 ординаты объекта Ь: 10, 10, 10 -зультат операции сложения объектов a i 4 Ь: 11. , 12, , 13 езультат операции сложения объектов а, b и с: 22, 24, 26 ' зультат выполнения операции вычитания с - а: 21, 22, 23 езультат выполнения операции вычитания с - Ь: 11, 12, 13
zoo Глава 1 1юрегрузка оператора, индексаторы и cbomcTJ Результат выполнения операции -а: -1, -2, -3 Результат выполнения операции а++: 2, 3, 4 Легко заметить, что операторы ++ и -- имеют префиксную и постфиксную фор^ Например, обе формы записи оператора ++0; И C + + ; являются правильными. Однако в результате перегрузки оператора ++ или — дгц обеих форм (постфиксной и префиксной) вызывается один и тот же метод. Пр| выполнении перегрузки невозможно указать разницу между префиксной и постфикс noil формами этих операторов. о Минутный практикум I. Что происходит при перегрузке операторов? Какое ключевое слово при это» \ II используется? q\ v) 2. Каков синтаксис перегружаемого бинарного оператора? 3. Что придает уникальность перегружаемым операторам ++ и —? Дополнительные возможности класса ThreeD Метод оператора может быть перегружен для любого класса или оператора. Давайп вернемся к классу ThreeD. Мы уже рассматривали перегрузку оператора г-, с помощьк которого суммировались координаты двух объектов, принадлежащих к классу ThreeD Однако это не единственный способ изменения значения каждой координаты объект! класса ThreeD. Еще, например, можно использовать метод, в котором к каждом^ значению координат прибавляется целочисленное значение. Для выполнения подобной? операции может понадобиться вторичная перегрузка оператора + (как показано ниже)! // Перегрузка бинарного оператора + , первый операнд которого является // объектом класса ThreeD, а второй имеет тип int. public static ThreeD operator + { ThreeD result = new ThreeDO; (ThreeD opl, int op2) result.x — opl.x 4- op2; result.у = opl.у + op2; result.z = op.z + op2; В этом методе переопределяется оператор + для выполнения операции сложения между операндами, один из которых имеет тип ThreeD, а другой принадлежит типу int. return result; } I. При перегрузке для оператора определяются действия, которые он будет выполнять по отношению к создаваемому классу. Используется ключевое слово operator. 2. Перегрузка бинарного оператора имеет следующий синтаксис: public static ret-type operator op(param-type1 operandl, param-type operand2) ( 11операции ) 3. Когда перегружается оператор ++ или —, методом operator изменяется значение операнда При перегрузке других операторов значение операнда не изменяется.
[1еРегРУзка опеРатоРов 259 Обратите внимание, что второй параметр принадлежит к целочисленному типу. Поэтому в приведенном выше методе допускается сложение значений каждого поля (переменной, определенной в классе) объекта типа ThreeD с целочисленным значе- нием. Эта операция допустима, поскольку при перегрузке бинарного оператора один из операндов должен принадлежать к типу класса, для которого перегружается данный оператор, а второй операнд может принадлежать к любому другому типу. Ниже приведена версия программы, включающая два метода, которые перегружают оператор + в классе ThreeD. /* В классе ThreeD перегружается оператор + для пары операндов, имеющих тип ThreeD, и для пары операндов, один из которых имеет тип ThreeD, а другой тип int.*/ using System; // Класс, содержащий координаты объекта в трехмерном пространстве, class ThreeD { тс х, у, z; // Координаты объекта - public ThreeDO { х - у = z - 0; } public ThreeD(int i, int j, int k) { x - 1; у j; z = k; } // Перегрузка бинарного оператора 4- для пары операндов, // принадлежащих к типу ThreeD. public static ThreeD operator +(ThreeD opl, ThreeD op2) ThreeD result = new ThreeDO; /* Сложение значений соответствующих координат двух объектов и возвращение результата. */ result.х =• opl.x + ор2.х; //В этих трех строках кода выполняется result.у = opl.у + ор2.у; // операция сложения целых чисел, для // которых оператор + сохраняет result.z = opl.z + op2.z; // свое исходное предназначение. return result; // Перегрузка бинарного оператора +, операндами которого в данном // случае являются объект класса ThreeD и переменная типа int. public static ThreeD operator +(ThreeD opl, int op2) ThreeD result new ThreeDO; result.x - opl.x + op2; result.у = opl.у + op2; result, z - opl.z + op2; return result; } // Отображение координат X, Y, Z. public void show() { Console.WriteLine(x 4- ”, ” + у + ”, + z) ;
260 Глаза 7. Перегрузка оператора, индексаторы и свойс' class ThreeDDemo { public static void Mam() { [w ThreeD a = new ThreeD (1, 2, 3); ThreeD b - new ThreeD(lC, 10, 10); fl ThreeD c new ThreeD (); fl Console.Write("Координаты объекта a: ”); fl a - show() ; ст Console.WriteLine(}; tl Console.Write ("Координаты объекта b: "); ft b- show () ; Jj Console.WriteLine (); X c - a + b; // Объект + объект. .t Console . Write ("Результат выполнения операции сложения " т jS ”объектов а и b: "); х с. show (); Л Console.WriteLine () ; с - b + 10; // Объект - целое число. * Console .Write ("Результат выполнения выражения b + 10: "); ‘fi, с.show (); "* 1 } Результат выполнения программы: .Координаты объекта а: 1, 2, 3 Координаты объекта Ь: 10, 10, 10 Результат выполнения операции сложения объектов а и Ь: 11, 12, 13 Результат вычисления выражения b -т 10: 20, 20, 20 Выводимые результаты подтверждают, если оператор + применяется к двум объектам, j то их координаты суммируются. Если оператор + применяется к объекту и целочис- 1 ленному значению, координаты объекта увеличиваются на это целочисленное зна- ) чсние. S 5 Хотя при перегрузке оператора - класс ThreeD приобретает некоторые полезные j свойства, для нормальной работы оператора требуется его вторичная перегрузка. < Причины этого в том, что для метода орсгзгсг+ (Thr-?eD, mt) допустимо и с Поль- ’ зованис операторов, подобных указанному ниже: Dbl =- оЬ2 4 10; Но для этого метода не допускается такая запись оператора: obi 10 + оЬ2; Дело в том, что в этом операторе целочисленный аргумент располагается слева от оператора +, а метод operator () ожидает, что он будет находиться справа. Для того чтобы можно было использовать обе формы записи оператора, потребуется перегрузить оператор + еще раз. При этом первый операнд должен принадлежать к типу int, а второй — к типу ТпгееГл То есть должна существовать возможность применения оператора г как для операции объект+целочисленное значение, гак и для
ререгрузка операторов 261 пСрании целочисленное значение+объект. Таким образом, двойная перегрузка опера- тор:1 г (”л” любого другого бинарного оператора) обеспечит использование встро- енного типа (в данном случае ±nt) как в левой, так и в правой части оператора. Ниже показана версия класса ThreeD, в которой оператор - перегружается согласно описанному алгоритму: ,, Пеоех’рузка оператора т для операций ’’объект + объект", "объект целое число” и "целое число + объект". */ usrr.g System; ;. к ;accz содержащий значения координат объекта в трехмерном /, ..ространстве. clas^ I'hreeD { in" к, у, z; // Координаты объекты. public ThreeDO { х - у - z С; } public ThreeDfint i, int j, inc k) { x •= i; у =- j; z => k; ’• /: Перегрузка бинарного оператора + для операции "объект +- объект". puolic static ThreeD operator + (ThreeD opl, ThreeD op2) ThreeD result = new ThreeDO; Сложение значений соответствующих координат двух объектов и возврат результата. */ result.х - opl.x + ор2.х; // В этих трех строках кода выполняется result.у = opl.у + ор2.у; // сложение целых чисел, для которых result.z = opl.z + op2.z; // оператор + сохраняет // свое исходное предназначение. return result- Перегрузка оператора, б результате которой ' можно выполнять операцию объект + цело- численное значение. '/’ Перегрузка бинарного оператора т для операции "объект + / целочисленное значение". public static ThreeD operator -t-(ThreeD opl, int op2) ThreeD result = new ThreeDO ; result. x opl.x r op2; result.у = opl.у + op2; result, z - opl. z -t- op2; Персфузка оператора, в результате которой можно выполнять операцию целочисленное значение + объект. return result; Перегрузка бинарного оператора + для операции > "целочисленное значение * объект". public static ThreeD operator + (int opl, ThreeD op2) ThreeD result = new ThreeDO; result.x - op2.x + opl; result.у = op2.у + opl; result.z - op2.z t opl;
262 Глава 7. Перегрузка оператора, индексаторы и свойсп return result; // Отображение координат X, Y, Z. public void show() { Console. WriteLine (x + ", " + У + ", " + z) * class ThreeDDemo { public static void Main() { ThreeD a = new ThreeD(l, 2, 3) ; ThreeD b new ThreeD(10, 10, 10); ThreeD c new ThreeDO; Console.Write("Координаты объекта a: a.show(); Console.WriteLine(); Console-Write("Координаты объекта b: "); b.show(); Console.WriteLine(); c^=aib; // Операция объект + объект. Console.Write("Результат выполнения операции а + b: "); с.show(); Console.WriteLine(); с = b + 10; // Операция объект + целочисленное значение. Console.Write("Результат выполнения операции b + 10: "); с . show(); Console.WriteLine(); с =- 15 + b; // Операция целочисленное значение + объект. Console.Write("Результат выполнения операции 15 + Ь: "); с.show(); } } Результат выполнения программы: Координаты объекта а: 1, 2, 3 Координаты объекта Ь: 10, 10, 10 Результат выполнения операции а + Ь: 11, 12, 13 Результат выполнения операции b + 10: 20, 20, 20 Результат выполнения операции 15 + Ь: 25, 25, 25 Перегрузка операторов сравнения Операторы сравнения, такие как == или <, также могут быть перегружены, причем этот процесс является очень простым. Как правило, перегруженные операторы сравнения возвращают значение true или false. В результате остается возможность обычного использования этих операторов, а перегруженные операторы сравнения могут использоваться в условных выражениях. Если возвращается какой-либо иной
Перегрузка операторов 263 тип результата, то изменяется первоначальное предназначение данного оператора, чго нежелательно. Ниже описана версия класса ThreeD, в которой выполняется перегрузка операторов и > Согласно перегруженным версиям этих операторов один объект считается больше другого, если все координаты данного объекта больше соответствующих координат второго объекта (и один объект считается меньше другого, если все его координаты меньше координат другого объекта). / *' В программе демонстрируется использование / ' перегруженных операторов ”<" и using System; // Класс, содержащий значения координат объекта в трехмерном /,* пространстве. class ThreeD { int х, у, z; // Координаты объекта public ThreeDO { х - у z - 0; } public ThreeD(int i, int j, int k) { x // Перегрузка оператора "<". public static bool operator < (ThreeD opl, ThreeD op2)4------ { if((opl.x < op2.x) && (opl.у < op2.y) && (opl.z < op2.z)) return true; else return false; Возвращает значение true, если объект opl меньше, чем объект op 2. // Перегрузка оператора ">". public static bool operator >(ThreeD opl, ThreeD op2) ◄----- if((opl.x > op2.x) && (opl.у > op2.y) && (opl.z > op2.z)) return true; else return false; Возвращает значение true, если объект opl больше, чем объект op 2. // Отображение координат X, Y, Z. public void show() Console. WriteLine (x + ”, " -t у + ", " + z) ; class ThreeDDemo ( public static void Main() { ThreeD a = new ThreeD(5, 6, 7); ThreeD b - new ThreeD(10, 10, 10); ThreeD c - new ThreeD(l, 2, 3); Console.Write("Координаты объекта a: "); a.show(); Console.Write("Координаты объекта b: "); b.show(); Console.Write("Координаты объекта с: ”); c. show () ; Console.WriteLine() ;
Глава 7. Перегрузка оператора, индексаторы и свойс- тва > с) 11(а < с) if(а > о) if (а < Ь) Console.WriteLine("Выражение а > с верно."); Console.WriteLine("Выражение а < с верно."); Console.WriteLine("Выражение а > Ь верно."}; Console.WriteLine("Выражение а < о верно."); Ридхлыат выполнения программы ординаты объекта а: 5, 6, 1 .pi ./п-юы объекта Ь: 10, 10, 1С Bupaxei .-.с; а < b верно. Пере, резку операторов сравнения необходимо осуществлять попарно (например сели перегружается оператор <, нужно также перегрузить оператор > и наоборот] В СЪ, как и в любом другом языке программирования, существуют следующие napi операт оров: й i Примечание Если перегружается пара операторов == и ! =, то обычно требуется переопределение^ методов Gbj ect. Equals () и ОЪт ect. GetHashCode (). Эти методы и технология их!, переопределения рассматриваются в главе 8. "j f-WWSlStfJ. Основные положения и ограничения при перегрузке j операторов | Действие перегруженного оператора, применимое к объекту класса, для которого он? определен, не связано со стандартным использованием оператора для встроенных’ типов О. Во избежание усложнения чтения программы по возможности перегру- жайте операторы так, чтобы они выполняли действия, для которых изначально вредна ^начались. Например, оператор +, перегруженный для работы с объектом K-iacca GhreeD, подобен оператору +, определенному для целочисленных типов данных. Если по отношению к какому-либо классу определить действия оператора + так, '[то вместо операции сложения будет выполняться операция деления, это просто приведет к путанице, не давая никаких преимуществ. Hepeiручка операторов связана с определенными ограничениями. Так, невозможно изменить приоритет операторов. Также запрещено изменение количества операндов, в коюрых нуждается оператор. Кроме того, существует множество операторов, к°1орыс не могут быть перегружены. В первую очередь к ним относятся оператор г|Рисваиван1)я и составные операторы присваивания, такие как Ниже приводится перечень других операторов, которые нс могут быть перегружены. О I I Typeof [J new is
265 ИнДеКсаторы йР Минутный практикум 1. Что требуется сделать, чтобы для переопределенного оператора была возмож- ен 41 ность указывать первым операндом переменную типа int, а вторым — объект необходимого класса и наоборот? 2. Какой тип данных обычно возвращают перегруженные операторы сравне- ния? 3. Можно ли перегружать оператор присваивания? Ответы профессионала Вопрос. Вы говорите, что составные операторы присваивания перегружать нельзя. Что произойдет, если в качестве операнда для составного оператора присваи- вания + = указать объект класса, для которого был определен оператор +? Ответ. В общем случае, если какой-либо оператор был переопределен, то при его использовании в составном операторе присваивания вызывается метод перегру- женного оператора. Таким образом, оператор += автоматически использует пользо- вательскую версию метода operator *(). Например, если при работе с классом 7r.r-aeD воспользоваться строками кода ThreeD а = new ThreeD(1, 2, 3); ThreeD b •=» new ThreeD(10, 10, 10); b - - а; //Выполняется операция сложения объектов а и b происходит автоматический вызов метода operator + () для объекта типа ThreeD, причем при его выполнении переменной b будут присвоены координаты 11, 12, 13. Индексаторы Вы знаете, что индексация массивов производится с помощью оператора [ . Этот оператор может быть перегружен для создаваемых вами классов, но в гаком случае нс будет использоваться метод operator. Вместо этого метода создастся индексатор. В результате к содержащемуся в объекте массиву можно осуществить доступ, указав имя объекта, а не имя массива. Основная область применения индексаторов — поддержка создания специализированных массивов, на которые накладывается одно или несколько ограничений. Индексаторы позволяют работать с любыми значения- ми, используя почти тот же синтаксис, что и при работе с массивами. Индексаторы могут иметь одно или несколько измерений. Начнем рассмотрение темы с одномерных индексаторов, которые определяются с использованием следующего синтаксиса: .ement-type this[int index} ( // Метод доступа get. get { // Возврат значения, указанного с помощью параметра index. 1 Оператор / требуется перегружать двумя методами, причем в одном случае объект должен быть вторым параметром, в другом случае — первым. ~. Операторы сравнения обычно возвращают значение типа bool. 3. Нет.
266 Глава 7. Перегрузка оператора, индексаторы и свойс > I // Метод доступа set Д set { "J // Присваивание (например, элементу массива) значения, // указаннох'о с помощью параметра index. • } 5 } ' 1 Здесь вместо словосочетания element-type указывается базовый тип индексатора. В результате каждый элемент, доступ к которому осуществляется с помощью индек» сатора. должен принадлежать к типу element-type (то есть будет выполняться доступ к массиву, элементы которого имеют указанный тип). Вместо параметра indej указывается индекс элемента, к которому производился доступ. Этот параметр Hit обязательно должен принадлежать к типу int, но поскольку индексаторы обычно применяются для индексирования элементов массива, чаще всего используется именно целочисленный тип. В составе индексатора определены два метода доступа, get и set. Структура метода доступа подобна структуре обычного метода за исключением того, что для метода доступа не указывается тип возвращаемого значения и он не имеет параметров. Оба метода доступа при использовании индексатора вызываются автоматически и при- нимают в качестве параметра значение, указанное вместо слова index. Если индек- сатор находится в левой части оператора присваивания, вызывается метод доступа set, а значение второго операнда присваивается элементу, указанному с помощью параметра index. Значение передается методу доступа с помощью неявного парамет- ра value (в приводимой далее программе обратите внимание на синтаксис приме- нения этого неявного параметра — переменная value нигде нс объявляется, и ее тип нигде не указывается). Если индексатор находится в правой части оператора при- сваивания, то вызывается метод доступа get, в результате выполнения которого возвращается значение, ассоциированное с параметром index. Одно из преимуществ, обеспечиваемых индексатором, заключается в том, что он гарантирует возможность точного контроля над доступом к массиву (предотвращается некорректный доступ). Например, использование индексаторов является наилучшим вариантом реализации массива с применением алгоритма предотвращения сбоев, созданного в главе 6. (При этом в код программы внедряется условный оператор if, проверяющий, не выходит ли указываемый индекс за пределы массива, то есть просто не допускается возникновение ошибочной ситуации.) Это достигается путем приме- нения индексатора, обеспечивающего доступ к массиву с помощью обычного син- таксиса, используемого при работе с массивами. // Для доступа к элементам массива в программе применяется индексатор, // в котором используется алгоритм предотвращения сбоев. using System; class FailSoEtArray ( int[] a; // Ссылка на массив. public int Length; // Переменная Length объявляется как public. public bool errflag; // Переменная возвращает значение, указывающее, // выходит индекс, передаваемый индексатору в качестве // параметра, за пределы массива или нет.
индексаторы 267 // Создание массива заданного размера. public FailSoftArray(int size) { a -= new int [size] ; Length = size; ; / Индексатор для класса FailSoftArray. public int this [int index] { -<------------| Индексатор для класса FailSof tArray. /! Метод доступа get. get { if (ok(index)) { errflag = false; return a[index]; } else { errflag = true; return 0; } } :i Метод доступа set. set { if (ok(index)) { a[index] = value; errflag = false; } else errflag = true; } // Возвращает значение true, если индекс находится в заданных // границах массива. private bool ok(int index) { if(index >= 0 & index < Length) return true; return false; ) / Показана работа с массивом с применением алгоритма предотвращения сбоев. lass ImprovedFSDemo { public static void Main() { FailSoftArray fs = new FailSoftArray(5); int x; // При указании индекса, выходящего за пределы массива, // выводится соответствующее сообщение. Console.WriteLine("Применение алгоритма предотвращения сбоев."); for(int i^O; i < (fs.Length * 2); i++) fs[i] = i*10;*<— -——-----------------———-рЗызов метода доступа set, j for(int i-*0; i < (fs.Length * 2); i++) { x = fs[i]; -4 ... ......-— ———-—— —Бызов метода доступа get. ] if (x != -1) Console.Write(x + " "); } Console.WriteLine() ; // Задается условие, приводящее к ошибке.
268 Глава 7. Перегрузка оператора, индексаторы и свой© Console.WriteLine(”\пСбой с сообщением об ошибке."Ъ; for(int i-0; i < (fs.Length * 2); 1 + +) { f s [ i ] - 1*10; if(fs.errflag) Console. WriteLine ("При обращении к элементу fs[" i + ", превышены граничные значения индекса массива."); for(int i^O; 1 < (fs.Length * 2); it+) { x - fs[i]; if(!fs.errflag) Console. Write (x + ” '*); else Console.Write(”\пПри обращении к элементу fs[" г i + "] превышены граничные значения индекса массива."); Результат выполнения программы: Применение алгоритма предотвращения сбоев. О 10 20 30 40 0 0 0 0 О Сбой с сообщением об ошибке. При При При При При О 10 20 30 40 обращении обращен-ии обращении обращении обращении элемент; элементу элементу элементу fs[5] fs [6] fs [7; fs[8] fs[9] превышены превышены превышены превышены превышены граничные граничные граничные граничные граничные индекса значения значения значения значения индекса индекса индекса индекса При При При При При обращении обращении обращении обращении обращении элементу элементу элементу элементу элементу fs [5] fs[6] fs[7] fs[8] fs[9] превышены превышены превышены граничные граничные граничные значения значения значения индекса индекса превышены превышены граничные граничные значения значения индекса индекса ма главы 6. В данн Этот результат исрсип используется индексатор, предотвращающий превышение граничных э нии индекса массива. Теперь подробнее рассмотрим организацию индекс: Объявление индексатора начинается со следующей строки: аналогичен результату выполнения программы из public int this[int index] { Здесь объявляется индексатор, который производит операции с элементами, и> шимм тип int. Индексатор объявляется как public и может использоваться ко. не относящимся к его классу. Ниже показан код метода доступа get: get ( if (ok(index)) { errflag = false; return a[index]; } else { errflag - true; return 0; Благодаря использованию метода доступа get предотвращаются ошибки, воз щие при превышении граничных значений индекса массива. Если указанный
Индрксаторы 269 родится внутри границ, возвращается элемент, соответствующий этому индексу. gcui индекс выходит за пределы массива, то выполняется строка кода return С;, т0 есть вместо элемента массива возвращается значение 0. В данной версии класса seftArray переменная errflag содержит результат проверки (то есть прове- ряется, выходит ли указываемый индекс за пределы массива). Это поле (переменная) мОжет проверяться после выполнения каждой попытки доступа для оценки ее успешности и для выведения информации об ошибке. (В главе 10 будет рассмотрен ёше один способ обработки ошибок с помощью подсистемы исключений С#, но на данном этапе достаточно использования флага ошибки, то есть булевой переменной Ниже приводится код метода доступа set, в котором также предотвращается превы- шение граничных значений индекса массива. set t1(ok(index)) ( a[index] = value errflag - false; else errflag = true; ) Если параметр index не выходит за пределы массива, значение, передаваемое неявным параметром value, присваивается соответствующему элементу. В против- ном случае параметру errflag присваивается значение true. Как вы помните, в методе доступа используется неявный параметр value, который содержит присваи- ваемое значение. Этот параметр не требует объявления. Методы доступа get и set не должны поддерживаться индексатором одновременно. Можно создать индексатор, предназначенный лишь для извлечения элемента, вы- полнив реализацию только метода доступа get. Можно создать индексатор, предна- значенный лишь для присваивания значения элементу массива, реализовав только метод доступа set. Индексатор не обязательно должен работать с массивом. Просто для тех, кто использует индексаторы, способ работы с элементами индексатора должен быть похож на способ работы с массивом. Например, в следующей программе используется индексатор, подобный массиву, который содержит степени числа 2 (от нулевой до пятнадцатой) и предназначен только для чтения. Однако обратите внимание на то, что массива в данном случае не существует. Вместо этого индексатор просто вычис- ляет подходящее значение, исходя из указываемого индекса. ' ' Индексаторы не обязательно должны работать с массивами. -ting System; 2-ass PwrOfTwo { ‘ * Доступ к логическому массиву, который содержит степени числа 2 (от нулевой до пятнадцатой). */ public int this[int index] { // Вычисление и возврат степени числа 2. get { if((index >- 0) && (index < 16)) return pwr(index); else return -1; }
Глава 7. Перегрузка оператора, индексаторы и свойо // Отсутствует метод доступа set. } int pwr(int р) { int result =- 1; for(int i=0; i<p; i++) result * = 2; return result; } } class UsePwrOfTwo { । public static void MainO { PwrOfTwo pwr = new PwrOfTwo (); Console.Write("Первые восемь степеней числа 2: "); forfint i=0; i < 8; i++) Console.Write(pwr[i] + " "); Console.WriteLine(); Console.Write("При указании индексов -1 и 17 возвращены "+ ? "результаты: "); ( Console .Write (pwr (-1 ] + " " t pwr[17]); s } • Результат выполнения программы: Первые восемь степеней числа 2: 1 2 4 8 16 32 64 128 j При указании индексов -1 и 17 возвращены результаты: “1 -1 Заметьте, что в индексаторе для класса PwrOfTwo присутствует метод доступа get, но отсутствует метод доступа set. Это означает, что индексатор предназначен только для чтения. Следовательно, объект класса PwrOfTwo может находиться в правой части оператора присваивания, но не может находиться в левой. Например, будет неудач- ной попытка использования в предыдущей программе оператора pwr[0] - 11; // Этот оператор не может быть скомпилирован. Использование этого оператора приведет к ошибке компиляции, потому что для индексатора не определен метод доступа set. При использовании индексаторов существует одно важное ограничение: для них не определена возможность применения параметра ref или out. Многомерные индексаторы Индексаторы могут создаваться и для многомерных массивов. Рассмотрим двухмер- ный массив (и двухмерный индексатор), в котором используется алгоритм предот- вращения сбоев. Обратите внимание на способ объявления индексатора. //В программе демонстрируется применение двухмерного индексатора, // использующего алгоритм предотвращения сбоев. using System; class FailSoftArray2D { int[,] a; // Ссылка на двухмерный массив.
cols; // Ряды и колонки двухмерного массива. Length; // Переменная Length объявляется открытой. int rows, public int public bool errflag; // В переменной errflag фиксируется результат // попытки доступа к элементу массива. . Создание массива по заданным размерам. public FailSoftArray2D(int r, int с) { rows - r; cols c; a -= new intfrows, cols]; Length - rows * cols; } // Индексатор для класса FailSoftArray2D. public int this [int indexl, int index2] { ◄-------—---J Двухмерный индексатор.^ // Метод доступа get. get { if (ok(indexl, index2)) { errflag = false; return a[indexl, index2]; } else { errflag = true; return 0; } } // Метод доступа set. set { if(ok(indexl, index2)) { a[indexl, index2] - value; errflag = false; ] else errflag - true; // Возвращение значения true, // если индексы не выходят за граничные значения индексов массива, private bool ok(int indexl, int index2) { if(indexl >= 0 & indexl < rows & index2 >- 0 & index2 < cols) return true; return false; Использование двухмерного индексатора. ass TwoDIndexerDemo { public static void MainO { FailSoftArray2D fs = new FailSoftArray2D(3, 5); int x; // При выполнении следующего цикла for происходит обращение к // несуществующим элементам массива, то есть возможно возникновение // ошибочной ситуации. Однако такая ситуация была обработана ранее - // при ее возникновении переменной errflag присваивается значение true и // возвращается значение 0. Console.WriteLine("При выходе за границы массива возвращается значение 0.");
« t. । leperрузка оператора, индексаторы и свой< for (int i~C; i < 6; i+ + ) fs[i, i] = 1*10; for(inc i=0; i < 6; i++) { x - fs [i,i] ; if(x != -1) Console.Write(x + " ") ; } Console.WriteLine(); ’/ Теперь переменная errflag используется как флаг ошибки, то есть /'/ является индикатором необходимости вывода сообщения. Console.WriteLine("ХпВыход за Гранины массива с сообщением об " + " ошибке; for(int i^O; i < 6; i+т) { fs [i, i] = i*10; if(fs.errflag) Console .WriteLine ("\пПри обращении к элементу fs[" + i -+ ”, " + i т ”] превышены граничные значения индексов массива. } for(int i-0; 1 < 6; i++) { x -= f s [ i, i ]; if(!fs.errflag) Console.Write(x + " ”) ; else Console.WriteLine{”\пПри обращении к элементу fs[” + L + ", ” * i + ] превышены граничные значения индексов массива. } i I Результат выполнения программы: При выходе за границы массива возвращается значение 0. С 1С 20 0 0 0 Выход за границы массива с сообщением об ошибке. При обращении к элементу При обращении к элементу При обращении к элементу 0 10 20 При обращении к элементу При обращении к элементу При обращении к элементу fs[3, 3] превышены граничные fs(4, 4] превышены граничные fs(5, 5] превышены граничные fs[3, 3] превышены граничные fs[4, 4] превышены граничные f s Г 5, 5 ] превышены граничные значения индексов массива значения индексов массива значения индексов массива значения индексов массива значения индексов массива значения индексов массива Минутный практикум Как называются методы выполняют? Можно ли определить индексатор, как предназначающийся только для доступа в индексаторе, и какие функции они чтения? 3. Можно ли создать индексатор для двухмерного массива? 1. 2. 3. Методы доступа называются get и set. Первый считывает значение указанного элемента массива, второй присваивает ему значение соответствующего типа. Да. Да.
Свойства 273 ^-Ответы профессионала----——--—....................... Вопрос. Могут ли быть перегружены индексаторы? Ответ. Да. Вызывается та версия, которая имеет наибольшее соответствие типов между его параметром и аргументом (или аргументами), используемым в качестве индекса. Свойства Еше одним специальным типом членов класса, имеющим сходство с индексаторами, является свойство, в котором поле связано с методом, обеспечивающим доступ к полю. В некоторых предыдущих программах возникала необходимость создания поля, хотя и доступного из других объектов, но такого, для которого выполнялся бы контроль над операциями с его значением (контроль того, какие операции с его значением возможны, а какие нет). Например, может потребоваться ограничение диапазона значений, присваиваемых этому полю. Конечно, для достижения подобной цели можно использовать закрытую переменную в сочетании с методом, обеспечи- вающим доступ к ее значению, но проще и лучше во всех отношениях использовать свойства. Свойство состоит из имени и методов доступа get и set. Методы доступа использу- ются для получения значения и присваивания его переменной. Основным достоин- ством свойства является то, что его имя может применяться в выражениях и операциях присваивания подобно обычной переменной, хотя на самом деле при этом автоматически вызываются методы доступа get и set. Такое использование свойства похоже на автоматический вызов методов get и set в индексаторе. Синтаксис свойства выглядит следующим образом: суре name | get { // Код метода доступа get. set { // Код метода доступа set. Здесь параметр type указывает тип свойства (например, тип int), а параметр name — имя свойства. После определения свойства любое использование идентификатора, Указанного вместо name, приводит к вызову соответствующего метода доступа. Метод доступа set автоматически получает параметр value, содержащий значение, которое присваивается свойству. Свойство управляет доступом к полю. Однако в свойстве поле не объявляется, поскольку оно должно быть создано вне свойства. В приводимой ниже простой программе определяется свойство тургор, которое обеспечивает доступ к полю prop. В данном случае свойство допускает присваивание полю только положительных значений. ' ' В программе демонстрируется использование простого свойства. vising System;
.«icpei оператора, индексаторы и своист| class SimpProp { int prop; // Поле, доступ к которому осуществляется с помощью метода // тургор, в котором также определяется, какие именно // значения могут быть присвоены данному полю. public SimpPropO I prop =- 0; } /* Свойство, обеспечивающее доступ к закрытой переменной экземпляра prop. Оно позволяет присваивать полю только положительные значения. */ . , ... ,_________________________ public int тургор { <- — —--------------| Свойство тургор управляет доступом к полю prop. get { return prop; I set [ if(value 0) prop = value; // Использование свойства. class PropertyDemo { public static void Main() { SimpProp ob = new SimpPropO; Console.WriteLine("Первоначальное значение свойства ob.тургор: " + ob.турrop) ; <--—— --------—— —[Свойство тургор используется подобно переменной?] ob.тургор - 100; //Свойству присваивается значение. Console-WriteLine("Значение свойства ob.тургор: ” + ob.тургор); // Полю prop невозможно присвоить отрицательное значение. Console-WriteLine ("Попытка присвоить свойству ob.тургор ’’ + " значение -10.") ; ob.тургор = -10; Console .WriteLine ("Значение переменной ob.тургор: " + ob.тургор); } } Результат выполнения программы; Первоначальное значение свойства ob.тургор: 0 Значение свойства ob.тургор: 100 Попытка присвоить свойству ob-тургор значение -10. Значение переменной ob.тургор: 100 Теперь тщательно проанализируем эту программу. В ней определяется одно закрытое поле prop и свойство тургор, управляющее доступом к этому полю (в свойстве не объявляется поле, а только происходит управление доступом к нему). Таким образом, свойство неразрывно связано с полем, объявленным в классе, где определено это свойство. Более того, поскольку поле prop является закрытым, доступ к нему может осуществляться только с помощью свойства тургор. Свойство тургор определяется как public, поэтому доступ к нему может осущест- вляться из других объектов. Такой подход является правильным, поскольку свойство обеспечивает доступ к полю prop, которое является закрытым. Метод доступа get просто возвращает значение поля, а метод доступа set присваивает переменной prop
Свойства 275 значение только в том случае, если оно является положительным. Таким образом, сВойство тургор контролирует значения, которые можно присвоить полю prop. Двойство тургор является свойством «чтение-запись», поскольку в нем определена возможность и получения, и присвоения значения. Кроме того, можно создавать свойства, так же как индексаторы, предназначенные только для чтения или только для записи. Для создания свойства, предназначенного лишь для чтения, нужно определить только метод доступа get. Для определения свойства, функционирующе- го исключительно в режиме записи, нужно определить только метод доступа set. Свойства можно использовать для улучшения программы, в которой обрабатывалась возможность возникновения ошибочных ситуаций. Ранее уже говорилось о том, что вес массивы ассоциированы СО СВОЙСТВОМ Length. При ЭТОМ В классе FailSoftArray использовалось открытое целочисленное поле Length. На самом деле это решение не является оптимальным, поскольку оно позволяет присваивать полю Length некоторое значение, отличное от длины массива. (Например, такое значение может быть преднамеренно искажено.) Эту проблему можно решить путем преобразования свойства Length в свойство, предназначенное только для чтения, как это показано в следующей программе. // В программе демонстрируется использование свойства Length, using System; class FailSoftArray { int[] a; II Ссылка на массив, int len; // Длина массива. public bool errflag; // В переменной errflag фиксируется результат // попытки доступа к элементу массива. // Конструирование массива заданного размера, public FailSoftArray(int size) { a = new int[size]; len = size; </ Свойство Length, предназначенное только для чтения._______________ public int Length { ◄----------^Теперь Length является свойством, а не полем. get { return len; } ''/ Индексатор для FailSoftArray. public int this[int index] { // Метод доступа get. get { if(ok(index)) { errflag =- false; return a[index]; ) else { errflag = true; return 0;
Глава 7. Перегрузка оператора, индексаторы и свой. // Метод доступа set. set { if(ok(index)) { a[index] - value; errflag = false; else errflag = true; // Возвращает значение true, если индекс не выходит за границы массива, private bool ok(int index) { if(index >- 0 & index < Length) return true; return false; // Использование свойства Length. class ImprovedFSDemo { public static void Main() { FailSoftArray fs = new FailSoftArray(5); int x; // Можно считывать значение свойства Length. for (int i==0; i < (fs.Length) ; 1++) f s [ i ] = i * 10; forfint i==0; i < (fs.Length); i++) [ x = fs[i] ; if (x I- -1) Console.Write (x + ’’ ”) ; } Console.WriteLine(); // fs.Length = 10; // He являясь комментарием, этот оператор будет // недействителен, поскольку для свойства не определен метод // доступа set. ) Итак, теперь Length является свойством, предназначенным только для чтения. Вы можете проверить его недоступность для изменения. Чтобы сделать это, удалите символ комментария, предшествующий оператору // fs.Length = 10; После попытки скомпилировать данный код программа выведет сообщение о том, что свойство Length предназначено только для чтения. Ограничения в использовании свойств Свойствам присущи некоторые ограничения, на которые следует обратить особое внимание. Во-первых, свойство не может быть передано методу в качестве параметра ref или out. Во-вторых, оно не может быть перегружено. (Вы можете определить два свойства, имеющих доступ к одной и той же переменной, но лучше не пользо- ваться этой возможностью.) Наконец, в-третьих, свойство не должно изменять
двойства 277 значение базовой переменной, доступ к которой контролируется свойством, при вызове метода доступа get. (Хотя создание такого метода доступа нс запрещено компилятором, метод get должен соответствовать своему первоначальному предна- значению — только считывать данные.) Минутный практикум 1. Можно ли объявить поле в свойстве? 2. Какие преимущества дает использование свойства? 3. Можно ли передать свойство методу в качестве параметра ref? I Проект 7-1. Создание класса Set j 0SetDevo.cs Как уже говорилось, перегрузка операторов, а также использование индексаторов и свойств облегчают создание классов, полностью интегрированных в среду програм- мирования языка С#. Обратите внимание, что с помощью переопределения требуе- мых операторов, индексаторов или свойств обеспечивается использование в програм- ме объекта, для класса которого и было выполнено переопределение, практически таким же образом, как используется встроенный тип. Работа с объектами такого класса выполняется при помощи операторов и индексаторов, причем эти объекты могут использоваться в выражениях. Включение свойств в класс обеспечивает интер- фейс, совместимый со встроенными объектами С#. Данный проект иллюстрирует создание нового класса и его интеграцию в среду С#. В проекте создастся класс Set, имитирующий множество, а также для этого класса перегружаются операторы, позволяющие выполнять операции над множествами (объединение, разность, добав- ление элемента и его удаление). Исходя из целей данного проекта определим множество как коллекцию уникальных элементов. Уникальность означает, что никакие два элемента множества нс могут совпадать. Порядок следования элементов множества нс имеет значения. Таким образом, множество Л, В, С} идентично множеству С, В} Множество также может быть пустым. Для множества могут определяться операции. В нашем проекте остановимся на иыборе следующих операций: j добавление элемента в множество; - > удаление элемента из множества; - > объединение множеств; - > разность множеств. • Нет, переменная должна быть объявлена в классе, в котором и определено свойство. Использование свойства позволяет применять для вызова метода доступа тот же синтаксис, что и при обращении к переменной. Нет.
278 Глава 7. Перегрузка оператора, индексаторы и свой| Добавление и удаление элементов множества нс требуют особых объяснений, поэтоцж подробно рассмотрим оставшиеся две операции. Объединением двух множеств называется множество, которое включает все элемент^ обоих множеств (конечно, в этом случае не допускается дублирование элементов)- Для объединения множеств в проекте используется оператор ъ. Разностью множеств называют множество, содержащее элементы первого множеств! которые не входят в состав второго множества. Для вычисления разности дв$ множеств в проекте используется оператор -. Например, если рассматривать разносу, двух множеств S1 и S1, то этот оператор удаляет элементы множества S2 из множества SI, помещая результат в множество S3: S3 SI - S2 Если множества S1 и S2 совпадают, множество S3 будет пустым. Конечно, для множеств могут быть определены и другие операции. Некоторые из них рассматриваются в разделе «Контрольные вопросы». Вы можете попытаться самостоятельно определить операции, которые будут полезны для работы с множе- ствами. С целью упрощения проекта класс Set будет состоять из наборов символов, но принципы, используемые при определении данного класса, могут применяться и при создании класса, предназначенного для хранения элементов других типов. Пошаговая инструкция 1. Создайте новый файл с именем SetDemo.cs. 2. Начните создание класса Set следующим образом: class Set { char [] members; // Этот массив содержит символы. int len; // Количество элементов массива. К каждому из символов, содержащихся в массиве, можно обратиться, указав имя массива и номер этого элемента. Число, указывающее количество элементов массива, хранится в переменной ten. 3. Добавьте в определение класса Set следующие конструкторы: // Создание пустого множества. public Set() { len = 0; } // Создание пустого множества заданного размера. public Set(int size) { members = new char[sizej; // Выделение памяти для множества. len =0; // Множество еще пустое. // Создание множества на базе другого множества (объекта класса Set). public Set(Set s) { members = new charts.len); // Выделение памяти для множества. for(int i=0; i<s.len; i++) members[i] = s[i]; Len = s.len; // Количество элементов.
279 Свойства Для создания множества определены три конструктора. Используя первый, можно создать пустое множество. В нем не создастся массив, содержащий элементы множества, а только инициализируется переменная len. При использовании второго конструктора создается пустое множество заданного размера. И наконец, множество можно создать на основе другого множества. В этом случае два множества содержат одни и тс же элементы, но являются различными объектами. д Добавьте свойство Length и индексатор, предназначенные только для чтения. // Определение свойства Length, предназначенного только для чтения, public int Length { get { return len; / Определение индексатора, предназначенного только для чтения, public char thislint idx){ get { if(idx >= 0 & idx < len) return members[idx]; else return (char)O; Свойство Length используется для указания длины массива. Индексатор возвра- щает элемент множества по указанному индексу. Проверка границ массива выполняется с целью предотвращения превышения граничных значений массива. Если индекс некорректен, возвращается символ 0. 5. Добавьте метод find(), код которого приведен ниже. Этот метод определяет, содержит ли множество символ, переданный ему с помощью параметра ch. Если этот элемент найден, метод возвращает индекс элемента, если нет — значение -1. /*Если элемент входит в состав множества, метод find возвращает индекс элемента, если не входит — возвращается значение -1, */ rnt find(char ch) { int i; for(i-0; iclen; i++) if(members[iJ — ch) return i; return -1; E Для выполнения операции добавления элемента в множество перегрузите опера- тор + для объектов класса Sec, как это показано ниже. // Добавление уникального элемента в множество public static Set operator +(Set ob, char ch) { Set newset = new Set(ob.len +1); // Увеличение числа элементов (I нового множества на единицу. // Копирование элементов. for(int i=0; i < ob.len; i++) newset .members [i] = ob .members [i] ; // Переменной len возвращаемого объекта присваивается значение // переменной len копируемого объекта, то есть передается значение // размера множества. newset.len - ob.len;
280 Глава 7. Перегрузка оператора, индексаторы и свой! // Проверяется наличие в множестве добавляемого символа. ж if(ob.find(ch) -= -1) { // Если элемент не найден, Ж // он добавляется в новое множество. 1 newset.members[newset.len] - ch; f newset. len + + ; f } I return newset; // Возврат нового множества (объекта класса Set) . } ; Рассмотрим работу перегруженного оператора подробнее. Во-первых, создаваем^ новое множество содержит элементы исходного множества, которое передаете^ помощью параметра ob, и новый элемент, который передается параметром Обратите внимание, что новое множество содержит на один элемент больше, че^ первоначальное множество ob, то есть добавление нового элемента учтено. Зат^ исходные элементы копируются в новое множество, а переменная 1еп, содержу, щая значение размера нового множества, становится равной переменной 1^, исходного множества. Добавление элемента ch в новое множество происходи только в том случае, если этот элемент еше не содержится в данном множеств (то есть вначале вызывается метод find о). При добавлении каждого нового элемента происходит приращение значения переменной 1еп на единицу. Посяе выполнения этой операции возвращается новое множество — объект newest, причем исходное множество нс изменяется. 7. Выполните перегрузку оператора в ходе выполнения которого должен удаляться элемент множества. Код перегруженного оператора приведен ниже. // Удаление элемента из множества. public static Set operator - (Set ob, char ch) { Set newset = new Set(); int i = ob.find(ch); // Если элемент не найден, // переменной i будет присвоено значение --1. // Копирование оставшихся элементов с использованием перегруженного // оператора + . for(int j=0; j < ob.len; j++) if(j 1— i) newset = newset + ob.members[j]; return newset; Вначале будет создано новое пустое множество, затем вызван метод find О, позволяющий определить, входит ли символ, передаваемый параметром ch, 8 состав исходного множества. Как вы помните, метод find О возвращает значе- ние -1, если символ, передаваемый параметром ch, не входит в состав множества. Затем все элементы исходного множества добавляются в новое множество, за исключением элемента, индекс которого равен значению, возвращаемому мето- дом find (). Таким образом, результирующее множество содержит все элементы исходного множества, исключая символ, переданный параметром ch. Если символ ch не является частью исходного множества, то возвращенное множество будел идентично исходному. 8. Теперь для выполнения операций объединения и разности множеств вновь перегрузите операторы + и как показано ниже. // Объединение множеств. public static Set operator +(Set obi, Set ob2) { Set newset — new Set (obi); // Копирование первого множества.
281 ^ойства // Добавление уникальных элементов из второго множества, for(int i=0; i < ob2.1en; i++) newset - newset + ob2[i]; return newset; // Возврат нового множества. / / Разность множеств. P^piic static Set operator - (Set obi, Set ob2) { Set newset - new Set(obi); // Копирование первого множества. // Вычитание из первого множества элементов второго множества, for (int i=0; i < ob2.1en; i++) newset - newset - ob2(i]; return nevjset; // Возврат нового множества. Как видите, для упрощения выполнения соответствующих операций эти методы используют ранее перегруженные версии операторов -г и При объединении множеств новое созданное множество вначале содержит элементы из первого множества, затем к ним добавляются элементы второго множества. Поскольку операция + выполняет добавление элемента в множество только в том случае, если он не является частью этого множества, результирующее множество представляет собой объединение двух множеств (без дублирования элементов). Оператор - удаляет соответствующие элементы. 9. Ниже приводится полный код класса Set и использование классом SetDemo перегруженных операторов и объектов, имитирующих множество. Проект 7.1 В программе создается класс Set, имитирующий множество и выполняющий операции над множествами. using System; class Set { char[] members; // Массив, содержащий символы (имитирующий множество) int Len; // Количество элементов множества. // Создание пустого множества. public Set() { ien - 0; } // Создание пустого множества заданного размера. public Set(int size) { members — new char[size]; // Распределение памяти для множества len - 0; // Элементы не были сконструированы } // Конструирование множества на базе другого множества. public Set(Set s) { members - new charts.Len); // Выделение памяти для множества.
282 Глава 7. Перегрузка оператора, индексаторы и cboi for(int 1=0; i < s.len; i++) membersfi] = s[i]; len - s.len; // Количество элементов множества. } // Определение свойства Length, предназначенного только для чтения, public int Length { get { return len; } // Определение индексатора, предназначенного только для чтения, public char this[int idx]{ get { if(idx >= 0 & idx < len) return members[idx]; else return (char)0; /*Если элемент входит в состав множества, метод find возвращает индекс элемента, если не входит, то возвращается значение -1, */ int find(char ch) { int 1; for(i=0; i < len; i++) if(members[i] == ch) return i; return -1; ) // Добавление в множество уникального элемента. public static Set operator +(Set ob, char ch) { Set newset = new Set(ob.len + 1); // Увеличение числа элементов // нового множества на единицу. // Копирование элементов. for(int 1=0; i < ob.len; i++) newset.members[i] = ob.members[i]; I/ Переменной len возвращаемого объекта присваивается значение li переменной len копируемого объекта, то есть передается значение // размера множества. newset.len = ob.len; // Выполняется проверка наличия в множестве добавляемого символа. if(ob.find(ch) == -1) ( // Если элемент не найден, // он добавляется в новое множество. newset.members[newset.len] - ch; newset. len-t-к ; } return newset; // Возврат нового множества (объекта класса Set). } // Удаление элемента из множества. public static Set operator -(Set ob, char ch) { Set newset = new Set();
283 }Оиства int i ob.find(ch); // Если элемент не найден, // переменной i будет присвоено значение -1. // Копирование оставшихся элементов с использованием перегруженного // оператора +. forfint j-0; j < ob.Len; j++) if (j i) newset - newset т ob.members [ j ]; return newset; J // Объединение множеств. public static Set operator +(Set obi, Set ob2) { Set newset = new Set(obl); // Копирование первого множества. // Добавление уникальных элементов из второго множества. for(int i=0; i < ob2.1en; i++) newset - newset + ob2[i]; return newset; // Возврат нового множества. } // Разность множеств. public static Set operator -(Set obi, Set ob2) { Set newset = new Set(obl); // Копирование первого множества. // Вычитание из первого множества элементов второго множества. for(int i^O; i < ob2.1en; i++) newset = newset - ob2(i]; return newset; // Возврат нового множества. } />' Использование класса Set. class SetDemo { public static void Main() { // Конструирование пустого множества. Set si =- new Set(); Set s2 =- new Set() ; Set s3 = new Set(); si - si + ' A ’ ; si = si + ’В’ ; si = si + ' C'; Console.Write("Множество si после добавления символов А, В и С: "); forfint i^O; i<sl.Length; i++) Console.Write (si [i] + " Console.WriteLine() ; si = si - ’В' ; Console.Write("Множество si после удаления символа В: ’’); forfint i-0; i<sl.Length; i++) Console.Write(si[i] + " ");
284 Глава 7. Перегрузка оператора, индексаторы и свой Console.WriteLine() ; si si - ' А * ; Console.Write("Множество si после удаления символа А: torfint 1-0; Ksl.Length; i-гт) Console.Write(si[ij + ” "); Console.WriteLine(); si - si - ‘C’; Console.Write("Множество si после удаления символа С: tor(int i-0; i<sl.Length; itr) Console.Write(si[i] t " "); Console.WriteLine("\n"); s 1 •- s 1 -» ' A' ; si - si + ' В' ; si si + 'C'; Console-Write("Множество si после добавления символов А, В и С: "); forjint i-0; i<sl.Length; !+♦) Console-Write(si[i] + " ”); Console.WriteLine(); s2 s2 -r ‘ A ‘ ; s2 - s2 + ’X’; s2 s2 + ’W‘; Console.Write("Множество s2 после добавления символов A, X и W: ’’) , for(int i=-0; Ks2. Length; 1+ + ) Console.Write(s2[i] т " "); Console.WriteLine(); s3 sl+s2; Console-Write("Результат объединения множеств si и s2: "); for (mt i-=0; i<s3.Length; i+-*-) Console.Write (s3 [i] + " '*); Console.WriteLine(); s3 -- s3 - si; Console.Write("Множество s3 после выполнения операции s3 - si: "); for(int 1-0; i<s3.Length; i^+) Console.Write(s3[i] + ” "); Console.WriteLine("\n"); s2 s2 s2; // Удаление всех элементов множества s2. s2 - s2 + 'С'; // Добавление символов А, В к С в обратном порядке. s2 - s2 + 'В' ; s 2 s 2 -г ' А ' ; Console.Write("Множество si теперь содержит символы: for(int i-G; i<sl.Length; i1♦) Console.Write(si[i; + " Console.WriteLine(); Console.Write("Множество s2 теперь содержит символы: "); forfint i-0; i<s2.Length; i Console.Wri.to (s2 [i] - " ");
________—??5 Console.WriteLine (); Console.Write("Множество s3 теперь содержит символы: for(inc i-0; i<s3.Length; i++) Console.Write (s3[1] + ” "); Console.WriteLine (); резУ;,ьтат выполнения программы: .n-вссство si после добавления символов А, В и С: АВС рожество si после удаления символа В: АС множество si после удаления символа А: С мужество si после удаления символа С: рожество si после добавления символов А, В и С: АВС рожество s2 после добавления символов А, X и W: А X W результат объединения множеств si и s2: А В С X W Множество s3 после выполнения операции s3 - si: X W Множество si теперь содержит символы: АВС Множество s2 теперь содержит символы: С В А Множество s3 теперь содержит символы: X W
286 Глава 7. Перегрузка оператора, индексаторы и сво| Контрольные вопросы 1. 2. 3. Предложите общую форму синтаксиса, используемого при перегрузке* операторов. Каков должен быть тип параметра метода operator? j Что нужно учесть при перегрузке оператора, чтобы его операндамич| могли быть объект необходимого класса и встроенный тип? Я Может ли быть перегружен оператор ? ? Можно ли изменить приоритет оператора? 4. Объясните, что такое индексатор. Приведите общую форму его синтак- сиса. 5. Какие функции выполняют методы доступа get и set в индексаторе? 6. Что такое свойство? Каков его синтаксис? 7. Можно ли объявлять переменную в свойстве? 8. Может ли свойство передаваться в качестве аргумента ref или out? 9. Для созданного в проекте 7-1 класса Set определите операторы < и > таким образом, чтобы они проверяли, является множество подмноже- ством или супермножеством для другого множества. Причем оператор < должен возвращать значение true, если множество, указанное слева от оператора, является подмножеством множества, находящегося справа от оператора, и возвращать значение false, если это условие не выполня- ется. Оператор > должен возвращать значение true, если множество, указанное слева от оператора, является супермножеством для множест- ва, находящегося справа от оператора, и возвращать значение false, если это условие не выполняется. 10. Для класса Set определите оператор &, выполняющий операцию пере- сечения двух множеств. 11. Попытайтесь определить другие операторы для класса Set. Например, попробуйте определить оператор |, выполняющий операцию «исклю- чающее ИЛИ» для двух множеств. (Множество, являющееся результатом выполнения операции «исключающее ИЛИ», состоит из элементов, не принадлежащих пересечению этих множеств.)
Наследование □ Основы наследования □ Использование модификатора protected □ Вызов конструкторов наследуемого класса □ Использование ключевого слова base □ Создание многоуровневой иерархии классов □ Ссылки на объекты наследуемого и наследующего классов □ Создание виртуальных методов □ Использование абстрактных классов □ Применение ключевого слова sealed □ Класс object
288 Глава 8. Наследование Напомним, что C# является объектно-ориентированным языком и поддерживает^ основные принципы ООП, одним из которых является наследование. Используя' наследование, вы можете создавать новые классы, представляющие собой расширен пис уже существующих классов. То есть класс может наследоваться другими класса- ми, обладающими некими уникальными свойствами, которые добавляются к уже имеющимся характеристикам и возможностям. Например, можно создать класс автомобиль, имеющий такие общие характеристики, как мощность двигателя,.расход топлива на 100 километров пробега, максимальная скорость, которую может развить автомобиль. Этот класс могут наследовать два других класса — грузовик, в котором появится характеристика грузоподъемность, и автобус, в котором появится характе- рце гика количество перевозимых пассажиров. В языке СИ класс, переменные и методы которого автоматически становятся членами нового создаваемого класса, называется наследуемым (базовым) классом. Класс, кото- рый к имеющимся членам наследуемого класса добавляет (определяет) новые члены класса, называется наследующим классом. Таким образом, наследующий класс явля- ется специализированной версией наследуемого класса. Наследующий класс насле- дует вес переменные, методы, свойства и индексаторы, определенные наследуемым классом, добавляя при этом свои собственные уникальные элементы. Основы наследования В C# при объявлении наследующего класса указывается имя наследуемого класса. Рассмотрим небольшую программу, в которой демонстрируются ключевые свойства наследования. В ней создается класс TwoDShape, хранящий значения ширины и высоты двухмерного объекта, и наследующий класс Triangle. Обратите внимание на способ объявления класса Triangle. //В программе создается простая иерархия классов, using System; // Класс, содержащий значения размеров двухмерной геометрической фигуры, class TwoDShape { public double width; public double height; public void showDim() { Console.WriteLine("Значения ширины и высоты геометрической фигуры = " + width + " и ” + height); } // Класс Triangle, наследующий class Triangle : TwoDShape { «<- public string style; public double areaO • return width ’ height / 2; класс TwoDShape. ------------Класс Triangle наследует класс TwoDShape. Обратите внимание на используемый синтаксис. Класс Triangle может ссылаться на члены класса TwoDShape так же, как если бы они были объявле- ны непосредственно в классе Triangle.~ public void showStyle() { Console.WriteLine("Вид треугольника " + style);
289 Основы наследоввния iss Shapes { .iblic static void Main() { Iriangle tl -=• new Triangle 0; Iriangle 12 - new Triangle (); tl.width - 4.0; 11 . heigh t - 4.0; ◄--- ----- tl. style =- "равнобедренный."; Вес члены класса Triangle, в том числе и ic, кото- рые были унаследованы из класса Two 1S.паре, дос- тупны для объектов типа Ti i angle. t2. width -= 8.0; 12.height == 12.0; 12.style - "прямоугольный."; Console.WriteLine("Информация об объекте tl: "); 11.showStyle(); t1.showDim(); Console. WriteLine ( "Площадь - " -r tl.areaO); Console.WriteLine(); Console.WriteLine("Информация об объекте t2: "); t2.showStyle(); 12.showDim(); Console.WriteLine ("Площадь - " +• t2.area()); Результат выполнения программы: /; формация об объекте tl: г.-д треугольника — равнобедренный. Ь.зчения ширины и высоты геометрической фигуры — 4 и 4 И.ощадь = 8 2- формация об объекте 12: -•<д треугольника - прямоугольный. >• ачения ширины и высоты геометрической фигуры - 8 и 12 .г-щадь - 4 8 В классе TwoDShape определены характеристики некоторой двухмерной геометриче- ской фигуры, которой может быть квадрат, прямоугольник и т. д. В классе Triangle, который наследует класс TwoDShape, определяется специфический тип геометриче- ской фигуры, в данном случае треугольник. В класс Tri angle включаются нее члены масса TwoDShape и добавляется поле style, а также методы area () и showStyle (). Название вида треугольника хранится в поле style, метод агез() вычисляет и 'извращает значение площади треугольника, а метод showStyle () отображает ка- чание вида треугольника. Обратите внимание на то. что синтаксис, используемый при наследовании, доста- точно прост. Имя наследуемого класса следует за именем наследующего класса, а чежду ними ставится двоеточие. Поскольку в класс Triangle включены все члены наследуемого класса Twor-Shape. но метод area () имеет доступ к значениям переменных wrdth и height. Кроме
290 Глава 8. Наслед) того, указав с использованием оператора точка (.) имя объекта tl или « переменную width или height, можно непосредственно внутри метода Maj обратиться к копиям этих переменных. На рис. 8.1 показано, каким образом TwoDShape включается в класс Triangle. 1 Несмотря на то, что класс TwoDShape является наследуемым для класса Triaty это полностью независимый автономный класс, который может использова самостоятельно. Например, действительным является следующий код: TwoDShape shape - new TwoDShape (); I shape.width - 10; si shape, height = 20; shape.showDim(); j Объект класса TwoDShape нс имеет доступа к любым членам наследующих клас Синтаксис объявления класса, наследующего другой класс, приведен ниже. class derived-class-name : base-class-name { * // тело класса »1 TwoDShape width height showDim() ► Triangle style area() showStyleO Рис. 8.1. Схема класса Triangle % Для каждого создаваемого наследующего класса можно указать лишь один насля мый класс (наследование нескольких классов в C# не поддерживается, в отлич| языка C++, поэтому при преобразовании кода C++ в код C# будьте очень ВНЙ| тельны). Однако в C# есть возможность создавать иерархию наследования, в ко11 наследующий класс сам может становиться наследуемым для другого класс? наследовать сам себя класс не может. J Огромное преимущество наследования заключается в том, что после создания КЛ1 определяющего некоторые общие характеристики, он может использоваться создания любого количества наследующих классов со специфичными характер! ками. Каждый наследующий класс может разрабатывать свою собственную кЛЧ фикацию. Например, ниже приведен код еще одного класса, наследующего Я TwoDShape. I //В этом классе, который наследует класс TwoDShape, определена новая J // характеристика, присущая прямоугольникам. ;] Class Rectangle : TwoDShape ( | public bool isQguareO { I if(width -- height) return true; Л return false; | ) I
.ноеы наследования 291 ,n’ic double area () ( pur- ге» urn width * height acC Rectangle включены все члены класса TwoDShape, а также метод is- square О , определяющий, является ли прямоугольник квадратом, и метод area О , „едяюший площадь прямоугольника. доступ к членам класса при использовании наследования g гпавс 6 уже мы уже говорили, что члены класса часто определяются как закрытые 1Я предотвращения их несанкционированного использования. При наследовании классов ограничения, имеющиеся при доступе к закрытым членам класса, нс снима- ются. Таким образом, метод наследующего класса нс имеет доступа к членам наследуемого класса, если они являются закрытыми. Приведем код новой версии класса TwoDShape, в котором переменные width и height по умолчанию определя- ются как private, следовательно, метод агеа() класса Triangle не имеет к ним доступа. // Эта программа не будет скомпилирована. Using System; // Класс, содержащий значения размеров двухмерной геометрической фигуры. Class TwoDShape { double width; // Теперь эти две переменные объявлены как закрытые. double height; public void showDim() { Console.WriteLine("Значения ширины и высоты геометрической фигуры = ” + width + ” и ’’ + height) ; ) Il Класс Triangle наследует класс TwoDShape. Class Triangle: TwoDShape [ ______________________________ Public string style; J Метод не имеет доступа к закрытым членам наследуемого класса. Public double area() { return width * height / 2; // Ошибка, метод не имеет доступа к этим // переменным. void showStyle () { Console. WriteLine ("Вид треугольника - " + style); n Рограмма не будет скомпилирована, поскольку при попытке обращения метода в0з '1 ’ определенного в классе Triangle, к закрытым переменным width и height ЗацНИК"СТ ошибка доступа. Как только члены класса weight и height становятся ^aPblTblMl1’ ДОСТУП к ним могут осуществлять только другие члены их собственного Сс£1’ '• Доступ членов наследующего класса становится невозможным.
292 Глава В. Наследова! Закрытый член класса недоступен для любого кода, определенного вне этого класД is том числе и кода наследующих классов. ж На первый взгляд данное правило кажется серьезным ограничением, но в Я существуют различные решения, позволяющие его обойти. Во-первых, есть возмож НОС1 ь использования членов класса, имеющих тип доступа protected (объявлений с модификатором protected). они будут описаны в следующем разделе. Во-вторьц для получения доступа к закрытым данным можно воспользоваться свойствахц объявленными как риЫ гс. В предыдущих главах уже рассказывалось о том, чтр программисты на C# обычно обеспечивают доступ к закрытым членам классу используя методы или преобразуя закрытые члены класса в свойства. Ниже приведу код еще одной версии класса TwolShupe, в которой члены класса width и height преобразованы в свойства. 3 программе демонстрируется использование свойств для доступа к закрытым членам класса. using System; , • Класс, содержащий значения размеров двухмерной геометрической фигуры, class TwoDShape { aouble pri_width; // Объявление закрытых переменных. double pri height; .'/ Свойства width и height. , _____________ public double width ( <------------| Определение свойств math и neight. | f. get ( return pri_wioth; } .) set { pri_wldth - value; } public double height { ----------------------—--- get { return pri height; } set { pri__height ~ value; } i public void showDim() { Console.WriteLine("Значения ширины и высоты iеометричесхой фигуры " + width + " и " т height); •/ В этом классе, наследующем класс TwoDShape, определены характеристики, // присущие треугольнику. class Triangle : TwoDShape { public string style; public double area() j _____________ ... Допускается использование свойств width и neight, поскольку они были объявлены как открытые. eturn width * height / public void showStyle() { Console.WriteLine("Вид треугольника style); class Shapes2 (
;^сНовы наследования 293 pUr?lic static void Main() { yriangle tl =- new Triangle (); priangle c2 - new Triangle (); . width - 4.0; •_1. height 4.0; '1.style - "равнобедренный."; '. width - 8.0; 2.height - 12.C; 2.style - "прямоугольный."; ,’cnsole .WriteLine ("Информация об объекте tl: "); ~. showStyle () ; 1. showDirr. () ; Tansole. WriteLine ("Площадь - " + tl.areaO); •ionsole .WriteLine () ; Console.WriteLine("Информация об объекте 12: ”); 1 2.showStyle(); 12 . showDirr. () ; Console.WriteLine("Площадь -- " + t2.area()); Ответы профессионала .....................-.........-.... О, Вопрос. В программировании на Java используются термины «суперкласс» и «подкласс». Употребляются ли эти термины в С#? Ответ. Класс, который в Java называется «суперкласс», в C# получил название «наследуемый класс». Класс, именуемый в Java «подкласс», в C# называется «насле- д\к>щий класс». Конечно, эти термины являются взаимозаменяемыми и могут использоваться в обоих языках, но в данной книге мы и далее будем применять стандартные термины С#. В языке C++ также используются термины «наследуемый класс», «наследующий класс». Минутный практикум 1. Как и где указывается наследуемый класс при объявлении наследующего класса? 2. Включаются ли в наследующий класс члены наследуемого класса? 3. Может ли наследующий класс получать доступ к закрытым членам своего наследуемого класса? Имя наследуемого класса указывается после имени наследующего класса и отделяется от него двоеточием. Да. Нет.
294 Глава 8. Наследс Использование модификатора protected В предыдущем разделе уже рассказывалось о том, что члены наследующего клас имеют доступа к закрытым членам наследуемого класса. Исходя из этого, м, предположить, что когда требуется обеспечить доступ к некоторым членам наел мого класса со стороны наследующего класса, эти члены класса обязательно дoJ быть объявлены с модификатором public. Но в таком случае данные члены к становятся доступными для любого кода, определенного вне наследуемого кд что в некоторых случаях совершенно нежелательно. К счастью, подобные вьц несколько преждевременны. В C# существует возможность создания защщце члена класса, что делает его открытым во всей иерархии наследования данного кт но оставляет закрытым для кода, определенного вне этой иерархии. Защищенный член класса создается с помощью указания модификатора prote. Если член класса объявляется как protected, то это равносильно объявл данного члена класса как закрытого, пока этот класс не станет наследуемы какого-нибудь класса. При наследовании класса А классом Б члены класса а, объ: ные как protected, становятся открытыми для кода класса Б, но остаются закр! для любого другого кода, определенного вне данной иерархии (то есть если к наследует класс Б, то его код получает доступ к защищенным членам класса А). Ниже приводится простая программа, в которой используется модификатор tected. //В программе демонстрируется использование модификатора protected, using System; class В { protected int i. Для полей i и j указан модификатор protected. Эти переменные является открытыми для членов класса D. public void set(int a, int b) { i = a; j = b; public void show() { Console.WriteLine(i + " " + j); class D : В { int k; // Закрытая переменная. //Метод класса D имеет доступ к переменным 1 и j, определенным в клас' public void setkO { k = i * j; -<-------- 1 Метод, определенный в классе р, имеет доступ к переменным i и j, поскольку они являются защищенными, но не закрытыми. public void showkf) ( Console.WriteLine(k); } i class ProtectedDemo (
сТрукторь| и наследование 295 риЫ1С D static void - new D(); Main() [ ob.set(2, 3); // Ob.show(); // Метод set() доступен из объекта класса D. Метод show() доступен из объекта класса D. ob seek О; // Эти обращения к методам экземпляра -также действительны. ob.showk() ; ) 1 Поскольку в этой программе класс в наследуется классом D, а переменные i и j объявлены в классе В как protected, метод setk () имеет к ним доступ. Как и при использовании модификаторов public и private, модификатор pro- tected остается присвоенным члену класса независимо от количества задействован- ных уровней наследования. онструкторы и наследование В иерархии наследования допускается, чтобы наследуемые и наследующие классы одновременно имели свои собственные конструкторы. Но при этом возникает вопрос, какой конструктор (наследуемого класса, наследующего класса или оба) будет отвечать за создание объекта наследующего класса. Ответить на этот вопрос можно так — каждый из конструкторов создает (инициализирует) свою часть объекта. Это разделение функций имеет смысл, поскольку наследуемый класс не имеет доступа к какому-либо члену наследующего класса, в результате объект создается по частям. В предыдущих примерах автоматически вызывались конструкторы, заданные по умолчанию, но это не лучший способ создания объектов. Как правило, в большинстве классов существуют свои конструкторы. Если в наследующем классе определен конструктор, процесс создания объекта не представляет особых сложностей, просто конструируется объект наследующего клас- са. Часть объекта, определенная в наследуемом классе, создается автоматически с помощью его конструктора, заданного по умолчанию. Ниже приводится модифици- рованная версия класса Triangle, в котором определен конструктор. При этом ооздается закрытое поле style, инициализируемое с помощью конструктора. В этой программе в класс Triangle добавлен конструктор. Usin9 Sysrem; // j, *viacc, содержащий значения размеров двухмерной геометрической фигуры. dSs TwoDShape { °uble pri_width; // Объявление закрытых переменных. Pri_height; // о- ~ ^ЬС;Котва width и height. U double width j • return pri_width; } > Se" pri_width - value; } Publ i... aouble height { 9er j u t return pri height; }
296 Глава 8. Насгедо, set i pri_height - value; } } public void showDiK() { Console . WriteLine ("Значения ширины и высоты геометрической фих'уры;" ъ width и ” + height) ; // В этом классе, наследующем класс TwoDShape, определены характеристики / ' свойственные треугольнику- class Triangle : TwoDShape [ string style; // Закрытая переменная. Я Конструктор. public Triangle(string s, double w, double h) { width — w; // Инициализация свойств, определенных в наследуемом клао Конструирование части объекта, которая была определена в классе TwoDShape. style = s; // Инициализация переменной, определенной в наследующем класс public double area() { return width * height / 2; public void showStyle() { Console.WriteLine("Вид треугольника - ” + style); class Shapes3 { public static vote MainO { Triangle tl = new Triangle (’’равнобедренный.", 4.0, 4.0); Triangle t2 - new Triangle("прямоугольный, 8.0, 12.0); Console-WriteLine("Информация об объекте tl: "); tl.showStyle(); t1.showDim(); Console-WriteLine ("Площадь - *’ - tl.areaO); Console.WriteLine() ; Console.WriteLine("Информация об объекте t2: "); t2.showStyle(); t2.showDim()- Console. Wri teLine ("Площадь - " -r t2.area()); } Конструктор класса Triangle инициализирует объявленное в нем поле styl- также наследуемые члены класса ТкоГ.'Зпаре. Если конструкторы определены и в наследуемом, и в наследующем классах, то пр® создания объекта несколько усложняется, поскольку в таком случае вызываются; конструктора. В подобной ситуации нужно применять ключевое слово base, КОТС
сТрукторы и наследование 297 отьзуется и для вызова конструктора наследуемого класса, и для обеспечения ^сту1111 к ,|ЛСНУ наследуемого класса, если он был скрыт при объявлении члена ri'civioincro класса. рь|3°в конструкторов наследуемого класса ^сПотьзуя расширенную форму объявления конструктора наследующего класса, в оТОром указывается ключевое слово base, можно вызывать конструктор, опреде- ^ецный в его наследуемом классе. Ниже представлен синтаксис этой расширенной формы об ья влепи я. ' ’a’',b’Crt/cCor (parameter-list) : base (arg-list) В перечне arc-list указываются любые аргументы, необходимые конструктору наследуемого класса. Обратите внимание на то. как используется двоеточие. Для приобретения навыков применения ключевого слова base используем в следую- щей программе еще одну версию класса TwoDShape, в котором определен конструк- тор, инициализирующий свойства width и height. II В программе используется новая версия класса TwoDShape, в который добавлен Ц конструктор. using System; И Класс, содержащий значения размеров двухмерной геометрической фигуры, class TwoDShape { double pri_width; // Объявление закрытых переменных. double pri_height; // Конструктор класса TwoDShape. pub.l: с I’woDShape (double w, double hl wiccii w; I/ Свойства width и height. Public double width { 9c; return pri_width; } s—- { pri_width = value; } I douole height { 9'- { return pri height; } Sr‘ ' pri__height - value; I void showDimO { '“•-''.sole. WriteLine ("Значения ширины и высоты геометрической фигуры - * width + ” и " + height) ; 7/ в . Il -'-мм классе, наследующем класс TwoDShape, определены характеристики, ‘Рвущие треугольнику.
298 Глава 8. Наследован! class Triangle : TwoDShape { string style; 11 Закрытая переменная. // Вызов конструктора наследуемого класса. public Triangle(strxng s, double w, double h) : base(w, h) { style - s; Л } Применение ключеного слова base /ия вызова конс1рук~юра I’woL’Shape. public double areal) { return width x height / 2; } public void showStyle() { Console.WriteLine("Вид треугольника - " + style); 1 } a } 1 class Shapes^ { j public static void Main() { Triangle tl = new Triangle("равнобедренный, 4.0, 4.0); Triangle t2 = new Triangle("прямоугольный, 8.0, 12.0); Console.WriteLine("Информация об объекте tl: "); tl.showStyle(); tl.showDim (); Console.WriteLine ("Площадь = " tl.areaO); Console.WriteLine(); Console.WriteLine("Информация об объекте t2: "); t2.showStyle(); t2.showDim(); Console.WriteLine("Площадь - " + t2.area()); } } В этой программе конструктор класса Triangle () с помощью ключевого слова base вызывает конструктор класса TwoDShape с параметрами w и h. В результате выпол- нения конструктора base (w, h) происходит инициализация свойств width и height указанными аргументами. То есть теперь конструктору класса Triangle не нужно инициализировать эти свойства, требуется лишь выполнить инициализацию пере- менной style, которая определена только в наследующем классе. Наследуемый класс может иметь несколько конструкторов, каждый из которых можно вызывать с помощью ключевого слова base. Вызывается тот конструктор, список параметров которого соответствует указанному списку аргументов. Ниже приводится код программы, в которой используются расширенные версии классов TwoDShape и Triangle. В этих классах определены конструкторы по умолчанию и конструкторы, принимающие один или несколько аргументов. // В программе используются расширенные версии классов TwoDShape и Triangle - using System; class TwoDShape { double pri width; // объявление закрытых переменных.
инструкторы и наследование 299 double pri__height; / ' Определение конструктора по умолчанию. public TwoDShape() { width - height = 0.0; / Конструктор с двумя параметрами. public TwoDShape(double w, double h) { width - w; height = h; / / Конструктор, предназначенный для создания объектов, у которых свойствам // width и height присваивается одно и то же значение. public TwoDShape(double х) { width = height = x; /.' Определение свойств width и height. public double width { get { return pri_width; } set { pri_width = value; } public double height { get { return pri_height; } set { pri_height = value; } public void showDimO { Console.WriteLine("Значения ширины и высоты геометрической фигуры == width -г " и " + height) ; //В этом классе, наследующем класс TwoDShape, определены характеристики, II свойственные треугольникам. class Triangle : TwoDShape { string style; // Закрытая переменная. /* Конструктор но умолчанию. Он автоматически вызывает конструктор по умолчанию класса TwoDShape. public Triangle() { style = "нуль."; Использование ключевого слова base для вызова различных кон- структоров класса TwoОShape. // Конструктор с тремя параметрами. public Triangle(string s, double w, double h) : base(w, h) { style =- s; // Конструктор, предназначенный для создания объекта, в котором определены // характеристики, свойственные равнобедренным треугольникам. public Triangle(double х) ; base(х) {
ow Глава 8. Наследов style - "равнобедренный."; ) p-jtlic douole areaO { return Audth ’ ne.ght 2; ntir'^tc void showStyle() { Console.WriteLine("Вид треугольника - " т style); Shapes5 { puolic static voia. Main() { Triangle tl - new Triangle (); lr*angle t2 - new Triangle ("прямоугольный, 8,0, 12.0); Triangle t3 - new Triangle(4.0); cl t2; Console.WriteLine("Информация об объекте tl: ’’) ; 11.showStyle (); tl.showDim(); Console.WriteLine("Площадь - " 4 tl.areaO); Console.WriteLine (); Console.WriteLine("Информация об объекте t2: "); c2.showStyle (); t2 . showDim () ; Console .WriteLine ("Площадь ’’ - t2.area()); Console.WriteLine(): Console . WrxteLine ( "Информация об объекте t3 : "); 13.showStyle () ; 13.showDim() ; Console.WriteLine("Площадь - ’’ + t3.area()); Console.WriteLine(); Результаты выполнения программы приведены ниже. Информация об объекте tl: Вид треугольника - прямоугольный. Значения ширины и -высоты геометрической фигуры -- 8 и 12 Площадь 48 Информация об объекте t2: Вид треуг'ольника 1:рямоуг ольный . Значения ширины и высоты геометрической фигуры - 8 и 12 Плошадь 48 Информация об объекте t3: Вид треугольника — равнобедренный.
укрытие переменных и наследование JU1 дначенИЯ ширины и высоты Р^ошадь — 8 геометрической фигуры 4 и 4 р-]рп указании в конструкторе наследующего класса ключевого слова case вызыва- лся конструктор наследуемого класса. То есть ключевое слово base всегда ссылается на наследуемый класс, который в иерархии наследования находится непосредственно н;11 вызывающим классом (вызывается конструктор класса, который непосредствен- нОбыл наследован вызывающим классом). Это справедливо даже для многоуровневой иерархии. Передача аргументов наследуемому конструктору осуществляется путем указания этих аргументов в скобках после ключевого слова base. Если в конструкторе вызывающего класса отсутствует ключевое слово base, то автоматически вызывается конструктор наследуемого класса, определенный по умолчанию. © Минутный практикум (Г; I. Каким образом из конструктора наследующего класса вызывается конструк- \ 3 ТОР наследуемого класса? 2. Могут ли параметры передаваться конструктору наследуемого класса? 3. Всегда ли ключевое слово base ссылается на конструктор непосредственно наследуемого класса? крытие переменных и наследование В C# члены наследующего и наследуемого классов могут иметь одно и то же имя. В гаком случае член наследуемого класса скрыт для других членов наследующего класса, и компилятор выводит предупреждение об этом на экран. Если вы намеренно скрываете член наследуемого класса и нс хотите, чтобы компилятор выводил преду- преждающее сообщение, то укажите перед членом наследующего класса ключевое слово new. Имейте в виду, что предназначение (выполняемые функции) ключевого слова new в данном случае отличается от его предназначения при создании экземп- ляра объекта. Ниже приводится программа, в которой демонстрируется скрытие переменной при наследовании. В программе демонстрируется скрытие переменной при наследовании, jing System; lass А { _____________________.___________________ public int i =- С • Переменная! в классе в скрывает переменную г, объявленную в классе А. Обратите внимание на ис- пользование ключевого слова new. / Наследующий класс. * lass В : А { new int i; // Эта переменная i скрывает переменную i, объявленную в классе А. public В(int b) < I, Для вызова конструктора наследуемого класса в конструкторе наследующего класса указы- вается ключевое слово base. 2. Да. 3. Да.
302 Глава 8. Наследован i - b; // Инициализация переменной i в классе В. } J public void show() { 4 Console-WriteLine ("Значение переменной i в наследующем классе: " + i); ’« } } £ class NameHiding { public static void MainO { В ob -- new В (2); ob, show () ; Л } ’ 1 Обратите внимание на использование ключевого слова new. Фактически оно указы: васт компилятору, что программист при создании новой переменной i намсрсннЬ скрывает переменную г, определенную в наследуемом классе а. Если ключевое слов| new нс будет указано, то компилятор выведет предупреждающее сообщение. ? Результат работы этой программы выглядит следующим образом: Значение переменной 1 ь наследующем классе: 2 После определения переменной т в классе в будет скрыта переменная т, определенна! в классе А. То есть при вызове метода show () для работы с данными объекта, имеющей тип в, будет отображаться значение переменной i, определенной! в классе в. | Использование ключевого слова base для доступа 1 к скрытой переменной ? Ранее уже говорилось, что ключевое слово bast применяется также для доступа К скрытым членам наследуемого класса. Используется следующая форма синтаксиса:! base.member Здесь слово member представляет метод или переменную экземпляра. При такогё использовании ключевое слово base выполняет практически те же функции, что И ключевое слово this, за исключением того, что слово base всегда ссылается на н а сл ед уе м ы й к л асе. Использование ключевого слова base для доступа к скрытой переменной демонст- рируется в следующей программе: // В программе демонстрируется использование ключевого слова base для // доступа к скрытой переменной. using System; class А { public int i = 0; } // Создание наследующего класса. class В : А { new int i; // Эта переменная i скрывает переменную i, объявленную в классе А. public B(int a, int b) {
^рытие переменных и наследование 303 base.i = а; // Обращение к переменной i, определенной в классе А, // с помощью ключевого слова base и оператора точка (.). j_ -= b; // Инициализация переменной i, определенной в классе В. } public void show() { !• Отображение переменной i, определенной в классе А. Console.WriteLine("Значение переменной i в наследуемом классе: " + base.r); / Отображение переменной i, определенной в классе В. Console.WriteLine("Значение переменной i в наследующем классе: " + i); } } class UncoverName { public static void Main() { В ob - new В (1, 2) ; ob.show(); Результат выполнения программы: Значение переменной i в наследуемом классе: 1 Значение переменной i в наследующем классе: 2 Хотя переменная экземпляра i в объекте ob скрывает переменную 1, определенную в классе а, использование ключевого слова base обеспечивает доступ к перемен- ной г, определенной в наследуемом классе. С помощью ключевого слова base также можно вызывать скрытые методы. Например, //В программе демонстрируется вызов скрытого метода с помощью ключевого слова // base. ising System; bass A { public int i 0; • / Метод show(), определенный в классе A. public void show() { Console.WriteLine("Значение переменной i в наследуемом классе: " + i); 7 Создание наследующего класса. -ass В : А { new int i; // Эта переменная i скрывает переменную i, объявленную в классе А. public B(int a, int b) { base.i a; // Обращение к переменной i, определенной в классе А, с помощью // ключевого слова base. i e Ь; // Инициализация переменной i, определенной в классе В. }
304 Глава 8. Наследов; // определенный в классе А. Гмсюд show () скрывает метод с таким же имснемТТ new public void show () { ----------*—jопределенный в классе A.~ I oase.showO; // Вызов метода show(), определенного в классе А. ~ -[Этот оператор вызывает скрытый метод show () /; Вывод значения переменной 1, инициализированной в классе В. Console.WriteLine("Значение переменной 1 в наследующем классе: " + i); class L’ncoverName { public static voia Main!) ( В ob new В (1, 2) ; op.show(); Результат выполнения программы: Значение переменной 1 в наследуемом классе: 1 Значение переменной i в наследующем классе: 2 Оператор base. show о ; вызывает метод show о, определенный в наследуемом классе. Ключевое слово new указывает компилятору, что при создании нового метода с.'.- w() программист намеренно скрывает метод show(), определенный в насле- дуемом классе А. Минутный практикум I. В наследующем классе объявляется переменная, которая будет скрывать д | переменную с таким же именем, определенную в наследуемом классе. Какое ХУ ключевое слово указывается перед именем переменной и се модификатором при ее объявлении? 2. Какое ключевое слово используется для обращения к скрытой переменной (скрытому методу) наследуемого класса? 3. Можно ли из наследующего класса вызывать скрытый метод, определенный в наследуемом классе? Проект 8.1. Расширение возможностей класса Vehicle 0 TruckDemo.cs В этом проекте мы займемся усовершенствованием и наследованием класса Vehicle, разработанного в главе 4. Напомним, что переменные класса Vehicle содержат следующую информацию об автомобиле — количество пассажиров, которое может 1. new. 2. base. 3. да.
укрытие переменных и наследование 305 рсрсвсзти автомобиль, запас топлива, а также расстояние (в милях), которое этот а111омобиль может проехать, используя один галлон топлива. Воспользуемся классом re: . cle как базовым для разработки более специализированных классов, например разработки класса Truck, который бы хранил информацию о таком тине чВ;омобиля, как грузовик. Важнейшей характеристикой грузовика является грузо- родьсмность, поэтому для создания класса Truck необходимо только наследовать класс Vehicle и добавить в определение класса конструктор, инициализирующий переменную, в которой будет храниться значение грузоподъемности. Чтобы усовер- шенствовать класс Vehicle, объявим его переменные закрытыми и определим свойства для доступа к этим переменным. Пошаговая инструкция I. Создайте файл TruckDemo.cs и скопируйте в него код последней версии класса . chicle из главы 4. 2. Создайте класс Truck, как показано ниже. .'/ Определение класса Truck, наследующего класс Vehicle, class Truck : Vehicle { int pri_cargocap; // Грузоподъемность (в фунтах). // Конструктор класса Truck. public Truck (int p, int £, int rn, int c) : base(p, f, m) { cargocap -- c; } // Свойство, обеспечивающее доступ к переменной pri_cargocap. public int cargocap { get { return pri__cargocap; } set | pri cargocap = value; ) } f Класс Truck наследует класс Vehicle, причем добавляется свойство cargocap. Таким образом, в класс Truck включена вся информация о транспортном сред- стве, определенная с помошью класса Vehicle. Добавлять в класс Truck следует только тс члены класса, которые придают ему специфические характеристики, делая его уникальным. 3. Объявите переменные класса Vehicle закрытыми, а затем переименуйте их. int ppi_passengers; // Количество пассажиров. int pri_fuelcap; // Емкость топливного Сака (в галлонах). int pri^mpg; // Расстояние (в милях), которое данный автомобиль // может проехать, используя один галлон топлива. 4 Добавьте свойства, определяющие доступ к этим переменным. // Свойства. public int passengers { get { return pri_passengers; } set { prijpassengers - value; } } public int fuelcap {
306 Глава 8. Наследова| get { return pri_fuelcap; } set { pri__fuelcap - value; public int mpg { get { return pri_mpg; } set { pri_mpg = value; } 5. Ниже приведен полный код программы, в которой на основе класса Vehicle помощью наследования создается новый класс Truck. Проект 8.1. В программе демонстрируется использование наследования для создания нового класса. using System; class int Vehicle { pri_passengers; pri_~fuelcap; pri_mpg; Количество пассажиров. Емкость топливного бака (в галлонах). Расстояние (в милях), которое данный автомобиль может проехать, используя один галлон топлива. is // Конструктор класса Vehicle. public Vehicle(int p, int f, int m) { passengers = p; fuelcap = f; mpg = m; // Метод возвращает значение максимального расстояния, которое может // проехать автомобиль с полным топливным баком. public int range () ( return mpg * fuelcap; // Метод вычисляет количество топлива, необходимое для преодоления // расстояния, значение которого передается методу в качестве параметра. public double fuelneeded(int miles) { return (double) miles / mpg; // Свойства. public int passengers { get { return pri_passengers; } set { pri__passengers = value; } public int fuelcap { get { return pri_fuelcap; } set { pri_fuelcap = value; }
укрытие переменных и наследование 307 public int mpg get { return pri_mpg; } set ; pri__mpg - value; } Определение класса Truck, наследующего класс Vehicle, class Truck : Vehicle { _nt pri cargocap; // Грузоподъемность (в фунтах). // Конструктор класса Truck. public Truck(int p, int f, int m, int c) : base(p, t, m) cargocap - c; '/ Свойство, обеспечивающее доступ к переменной pri__cargocap. public int cargocap { get { return pri_cargocap; } set { pri_cargocap - value; } class TruckDemo { public static void Main() { // Создание некоторых объектов класса Truck. Truck semi =- new Truck(2, 200, 7, 44000); // Автофургон. Truck pickup - new Truck(3, 28, 15, 2000); // Пикап, double gallons; int dist -= 252; gallons = semi.fuelneeded(dist) ; Console.WriteLine("Грузоподъемность автофургона " + semi.cargocap + *’ фунтов."); Console. Wri teLine ("Чтобы проехать ’’ + dist r " мили, автофургону " + "требуется " + gallons + " галлонов топлива.\n"); gallons -- pickup.fuelneeded(dist); Console. WriteLine ( "Грузоподъемность пикапа -= " + pickup. cargocap -r " фунтов . ’’) ; Console. Wri teLine ("Чтобы проехать " -f- dist + " мили, пикапу " + "требуется ’’ + gallons + " галлона топлива.\n”); Г Ы Результат выполнения программы: Грузоподъемность автофургона - 44000 фунтов. Чтобы проехать 252 мили, автофургону требуется 36 галлонов топлива. Грузоподъемность пикапа -= 2000 фунтов. Чтобы проехать 252 мили, пикапу требуется 16,8 галлона топлива.
308 Глава 8. Наследо, 7. Класс Vehicle могут наследовать многие другие типы классов. НапримегЯ следующем фрагменте кода приведены первые строки определения класса, коЯ рый содержит информацию о клиренсе внедорожного автомобиля. Ж // Создание класса, содержащего характеристики внедорожного автомобиля Ж class OffRoad : Vehicle { к int groundclearance; // Величина клиренса (в дюймах). Ж И ... 1 } -ж Изучив вышеизложенный материал, сделаем выводы. Итак, используя наследован^ вы можете создавать новый класс, в котором определены общие черты какого-лй|) объекта, эти черты могут быть унаследованы новым классом, в который добавляю» его уникальные характеристики. п £ Создание многоуровневой иерархии классов | До настоящего момента мы использовали простую иерархию классов, состоящую» наследуемого и наследующего классов. В C# можно также создавать иерархА содержащую любое количество уровней наследования. Ранее говорилось, что мол® использовать наследующий класс как базовый для дальнейшего наследования. В^> чсствс примера рассмотрим три класса — А, в и с. Пусть класс с наследует классу который в свою очередь наследует класс А. В подобной ситуации каждый наследую- щий класс наследует все черты, характерные для его наследуемого класса. В данном случае класс с наследует все характеристики классов в и А. В следующей программе демонстрируется использование многоуровневой иерархии. Здесь класс Triangle уже является базовым при создании класса ColorTriangle, который наследует все характеристики классов Triangle и TwoDShape. В классе ColorTriangle объявлена новая переменная color, содержащая информацию О цвете треугольника. // Многоуровневая иерархия классов. using System; class TwoDShape { double pri__width; // Объявление закрытых переменных, double pri__height; // Определение конструктора no умолчанию public TwoDShape() { width = height =* 0.0; } // Конструктор с двумя параметрами. public TwoDShape(double w, double h) { width - w; height = h; // Конструктор, предназначенный для создания объектов, у которых свойства)* // width и height присваивается одно и тоже значение. public TwoDShape(double х) {
«здание многоуровневой иерархии классов 309 height п double height [ • i { return pri_height; } { pri height - value; } p:ji-..lo void showDim () ( Console . Wri teLine ("Значения ширины и высоты геометрической фигуры width т " и " + height) ; i / В этом классе, наследующем класс TwoDShape, определены характеристики, // свойственные треугольникам. las.- Triangle : TwoDShape i st: .ng style; !/ Закрытая переменная. /’ Конструктор по умолчанию. Он автоматически вызывает конструктор ко умолчанию класса TwoDShape. piclie Triangle() j 1 у 1 e - ” ну л ь ’’ ; конструктор с тремя параметрами. pi'_ic Triangle (string s, double w, double h) : base(w, h) { t-yle - s; Конструктор, предназначенный для создания объекта, в котором определены характеристики, свойственные равнобедренным треугольникам. F.-i'lic Triangle (double х) : base(x) { style - ’’равнобедренный"; t •- lie double area() J return width * height / 2; •-ic voio showStyle() i Console.WriteLine("Вид треугольника - ’’ Liacc ColorTriangle, наследующий класс Triangle. a‘;s ColorTriangle : Triangle {<- string color; Класс ColorTriangle наследует класс Triangle, который в свою очередь наследует класс rwoJF.hape, поэтому в классе ColorTriangle присутствуюI вес члены классов Triangle и TwoDShape.
310 Глава 8. Наслвдое; public ColorTriangle(string c, string s, double w, double h) : base(s, w, h) { color -- c; } // Вывод информации о цвете треугольника. public void showColor() { Console .WriteLine ( "Цвет треугольника ’’ -t- color); class Shapes6 { public static void Main() { ColorTriangle tl - new ColorTriangle("голубой", "прямоугольный", 8.0, 12.0); ColorTriangle t2 - new ColorTriangle("красный", "равнобедренный", 2.0, 2.0); Console.WriteLine("Информация об объекте tl: ") ; tl.showStyle(); tl.showDim(); tl.showColor(); Console .WriteLine ("Площадь = " + tl.areaO); Console.WriteLine(); Console.WriteLine("Информация об объекте t2: "); t2.showStyle(); t2 .showDim(); t2.showColor(); Console.WriteLine("Площадь - " + t2.area()); Объект класса ColorTriangle может вызывать методы, определенные в нем, а также в его наследуемых классах. Результат выполнения программы: Информация об объекте tl: Вид треугольника — прямоугольный Значения ширины и высоты геометрической фигуры ~ 8 и 12 Цвет треугольника — Голубой Площадь ~ 48 Информация об объекте t2: Вид треугольника - равнобедренный Значения ширины и высоты геометрической фигуры = 2 и 2 Цвет треугольника - красный Площадь « 2 Поскольку при создании класса ColorTriangle применяется многоуровневое на- следование, объекты этого класса могут использовать все члены классов Triangle и TwoDShape. При определении класса ColorTriangle добавляется только строковая переменная, необходимая для его специфической цели — возможности указать цвет треугольника. Наследование обладает тем преимуществом, что позволяет повторно использовать код. Отметим, что данная программа демонстрирует еще одну важную особенность " ключевое слово base всегда ссылается на конструктор ближайшего наследуемого
^оГда вызываются конструкторы 311 ,паСса. Если ключевое слово base указывается в классе ColorTriangle, то вызыва- лся конструктор класса Triangle. Если же ключевое слово base указывается в ласс'с Triangle, то вызывается конструктор класса TwoDShape. Когда конструктору с1 едусмого класса необходимы какие-либо параметры, то в соответствии с прави- чами создания многоуровневой иерархии классов все наследующие классы должны передавать эти параметры «вверх» на соответствующий уровень, причем вне зависи- мости оз того, нужны ли эти параметры самому наследующему классу. Когда вызываются конструкторы При изучении предыдущей темы мог возникнуть вполне закономерный вопрос о том, какой конструктор при создании наследующего класса вызывается первым — конст- руктор наследующего класса или конструктор наследуемого класса. В иерархии классов конструкторы вызываются в том порядке, в котором выполнялось наследо- вание — от наследуемого класса к наследующему. Более того, этот порядок остается неизменным независимо от того, используется ключевое слово base или нет. Если это ключевое слово не указывается, вызывается заданный по умолчанию конструктор (без параметров) каждого наследуемого класса. Порядок вызова конструкторов де- монстрируется в следующей программе: // В программе демонстрируется порядок вызова конструкторов. using System; // Создание наследуемого класса. class А { public А() { Console.WriteLine("Метод, определенный в классе А."); !> Создание класса, наследующего класс А. class В : А { Public В() { Console.WriteLine("Метод, определенный в классе В."); создание класса, наследующего класс В. С : в { Public С() { Console.WriteLine("Метод, определенный в классе С."); "'-ss OrderOfConscruetion { Public static void Main() { С c « new CO;
312 Глава 8. Наследова! В результате выполнения этой программы будут выведены следующие строки: я Метод, определенный к классе А. Метод, определенный в классе В. Я Метод, определенный в классе С. J1 Как видите, конструкторы вызываются в порядке наследования, и это оправдано Я поскольку любая инициализация, выполняемая в наследуемом классе, может служим основой для инициализации, выполняемой наследующим классом. Поэтому констЗ руктор наследуемого класса должен вызываться первым. j Ссылки на объекты наследуемого и I наследующего классов I Как вы уже знаете, C# является языком, требующим строгого соблюдения типа! (например, при выполнении операции присваивания). Автоматическое преобразовав нис типа, выполняющееся при работе и переменными обычных типов, не распро-1 стран яется на переменные ссылочного типа. То есть ссылочная переменная одного! класса не может ссылаться на объект другого класса. Рассмотрим следующую про-$ грамму: | // Эта программа не может быть скомпилирована. f class X { int а; public X(int 1) { a -=- i; ] } class Y { int a; public Y(int i) { a ~ i; } } class IncompatibleRef { public static void Main() { .5 X x new X(1C); V? X x2; £ Y у - new Y(5); У 1 x2 - x; // Эта операция присваивания действительна, поскольку в ней £ // участвуют ссылочные переменные одного типа. х2 - у; // Ошибка, типы ссылочных переменных не совпадают. * } ~--------------1 I Э1и ссылочные переменные несовместимы. * В этой программе, даже если классы х и Y физически совпадают, ссылочной « переменной типа х невозможно присвоить ссылку на объект типа Y. То есть ссылочная переменная может ссылаться только на объекты одного типа. | Однако существует важное исключение в строгой типизации С#. Ссылочной пере- f менной наследуемого класса может быть присвоена ссылка на объект любого насле- V дующего класса. Рассмотрим, как выполняются эти операции. л
£СЬ1ЛКИ на объекты наследуемого и наследующего классов // Ссылочная неременная наследуемого класса может ссылаться на объект наследующего класса. System; - - < X 1 - pjtllc int a; pur lie X(int i) { puo^ic int b; public Yfint i, int j) : base(j) { class BaseRef { public static void Main() { X x - new X(10); X x2; Y у - new Y(5, 6); x2 = x; // Это действительная операция, поскольку задействованы // переменные одного и того же типа. Console.WriteLine("Значение переменной х2.а: ” + х2.а); ~~~ ~ ~~~ ' —Поскольку класс у наследует ю х2 - у; // Также действительная операция, ссьыочная переменная х2 м< , , жет ссылаться на объект у. // поскольку класс Y наследует класс Х.^—' —- - - ——— Console.WriteLine("Значение переменной х2.а: " + х2.а); // Ссылочная переменная типа X "знает" только о членах класса X. *2.а - 19; // Действительное обращение. х2.Ь = 27; // Ошибка, в классе X не определена переменная Ь. В ной программе класс Y наследует класс X; таким образом, переменной х2 мо» Di>i]b присвоена ссылка на объект у. Важно понимать, что возможность доступа к членам класса зависит именно от ти ссылочной переменной, а не от типа объекта, на который она ссылается. То есть ес. ссылочной переменной, имеющей тип наследуемого класса, присваивается ссыл Н;! объект наследующего класса, то с помощью этой переменной можно буд вращаться только к тем членам класса, которые были определены в наследуемс классе. Именно поэтому ссылочная переменная х2 не имеет доступа к переменнс экземпляра ь даже после того, как переменной х2 была присвоена ссылка на объе, класса у. Такое ограничение имеет смысл, поскольку наследуемый класс ничего г ’ <наст» о тех членах класса, которые были добавлены при определении наследуюшс! класса. Поэтому, чтобы приведенная выше программа могла быть скомпилирован! се последний оператор был закомментирован.
314 Глава 8. Наследован Довольно часто в классе определяется конструктор, который принимает в качеств» параметра объект собственного класса. В результате класс может конструировать копию объекта. Также при вызове конструкторов в иерархии классов (при передач^ конструкторам объектов в качестве параметров) ссылочной переменной, имеющей тип наследуемого класса, присваивается ссылка на объект наследующего класса! Например, обратите внимание на следующие версии классов TwoDShape и Triangle 1 В обеих версиях есть конструкторы, использующие объект в качестве параметра. f // Передача ссылочной переменной наследуемого класса ссылки на объект // наследующего класса, using System; class TwoDShape { oouble pri_width; //Объявление закрытых пер< double pri_height; // Определение конструктора по умолчанию. public TwoDShape() { width = height = 0.0; ) // Конструктор с двумя параметрами. public TwoDShape(double w, double h) { width - w; height - h; } // Конструктор, предназначенный для создания // width и height присваивается одно и то же public TwoDShape(double х) { width = height - x; } // Конструирование объекта на основе другого public TwoDShape (TwoDShape ob) {◄---•-------- width = ob.width; height - ob.height; } //Определение свойств width и height, public double width { get { return pri width; ) set { pri__width -- value; } } public double height { get { return pri_height; } set { pri_height - value; } объектов, у которых свойствам £ значение. -к объекта. Конструирование объекта на основе объекта этого же класса public void showDim() { Console.WriteLine("Значения ширины и высоты геометрической фигуры - " + width + *' и " + height) ;
Ссылки на объекты наследуемого и наследующего классов 315 у/ В этом классе. наследующем класс TwoDShape, определены характеристики. // свойственные треугольникам. class Triangle : TwoDShape { string style; // Закрытая переменная. // Определение конструктора по умолчанию, public Triangle () { style - "нуль"; // Конструктор с тремя параметрами, вызывающий конструктор наследуемого // класса. public Triangle(string s, double w, double h) : base(w, h) { style - s; } // Конструктор, предназначенный для создания объекта, в котором определены // характеристики, присущие равнобедренным треугольникам. public Triangle(double х) : base(x) { style - "равнобедренный"; // Конструирование объекта на основе другого объекта, public Triangle(Triangle ob) : base(ob) {ч- style == ob.style; Передача ссылки на объект класса Triangle конструктору класса TwoDShape. public double area() { return width * height / 2; } public void showStyle() { Console . Wri teLine ("Вид треугольника - " + style); class Shapes? { public static void Main() { Triangle tl - new Triangle("прямоугольный", 8.0, 12.0); // Копия объекта tl. Triangle t2 = new Triangle(tl); Console.WriteLine("Информация об объекте tl: "); t1.showStyle(); tl.showDim(); Console. WriteLine ("Площадь - " + tl.areaO); Console.WriteLine(); Console.WriteLine("Информация об объекте - t2: "); t2.showStyle(); t2.showDim(); Console.WriteLine ("Площадь =- " + t2.area());
316 Глава 8. Наследова В программе объект 12 конструируется на основе объекта : и о.? идентичны. Результат выполнения следовательно, объе п ро гра м м ы п р и веде н н и же: Прям. Информация об объекте t2: Вид треугольника ирямоуролдный Значения ширины и высоты геометр) Площадь - 4 8 фигуры Обратите особое внимание на конструктор класса т конструирование основе другого объекта. : base(ob) { ~ ob.style; Этот конструктор принимает объект типа ключевого слова base конструктору класса руктора: е и передаст его с помощней? те. Приведем код этого к о нет-# // Конструирование объекта на основе другого объекта, public TwoDShape(TwoDShape ob) { wiath - ob.width; heignt =- ob.height; Обратите внимание, что конструктор TwoDShape() ожидает получить в качестве;1 2 3 4 аргумента объект класса TwoDShape, но конструктор Triangle () передает ему объек^' класса Triangle. Как говорилось ранее, это происходит в силу того, что ссылочной? переменной, имеющей тип наследуемого класса, может быть присвоена ссылка н® объект наследующего класса. То есть передача ссылочной переменной ob, имеющей в конструкторе TwoDShape () тип TwoDShape, ссылки на объект оо класса Triangl?, является действительной операцией. Поскольку конструктор TwoDShape () инициа-. лизирует только члены класса TwoDShape, его не «интересует» то, что объект может содержать другие члены класса, добавленные при определении наследующих классов. ® Минутный практикум )- Может ли наследующий класс использоваться в качестве базового класса I9 X ’1 для Другого наследующего класса? \ 2. В каком порядке вызываются конструкторы в иерархии классов? 3. Предположим, что класс Лег наследует класс Airplane. Можно ли присво- ить ссылочной переменной типа Airplane ссылку на объект типа Det? 1. Да. 2. Конструкторы вызыв<1КУ1Ся в порядке наследования. 3. Да. Во всех случаях ссылочная переменная типа наследуемого класса может ссылаться на объект наследующею класса, но не наоборот.
виртуальные методы и переопределение 317 0ИртУальные методы и переопределение fvicroa. при определении которого в наследуемом классе было указано ключевое слово .= 1. и который был переопределен (о переопределении метода будет рассказано рстелуюшсм абзаце) водном или более наследующих классах, называется виртуаль- ным методом. Следовательно, каждый наследующий класс может иметь собственную версию виртуального метода. В C# выбор версии виртуального метода, которую Треб\ется вызвать, осуществляется в соответствии с типом объекта, на который ссылается ссылочная переменная. Этот выбор (определение) осуществляется во время выполнения программы. Ссылочная переменная может ссылаться на различные типы объектов, следовательно, могут быть вызваны различные версии виртуальных мето- дов. другими словами, именно тип объекта, на который указывает ссылка (а не тип ссылочной переменной), определяет вызываемую версию виртуального метода. Та- ким образом, если класс содержит виртуальный метод и от этого класса были наследованы другие классы, в которых определены свои версии метода, при ссылке переменной типа наследуемого класса на различные типы объектов вызываются различные версии виртуального метода. При определении виртуального метода в сое I а вс наследуемого класса перед типом возвращаемого значения указывается ключевое слово virtual, а при переопределении виртуального метода в наследую- щем классе используется модификатор override. Процесс определения виртуально- го метода внутри наследуемого класса, при котором частично или полностью изме- няется тело метода, а имя, параметры и их типы остаются прежними, называется переопределением метода. Виртуальный метод нс .может быть определен с модифика- тором static или abstract (использование этих модификаторов рассматривается далее в этой главе). Переопределение метода положено в основу концепции динамического выбора вызы- ваемого метода. Это механизм, с помощью которого выбор вызываемого переопре- деленного метода осуществляется во время выполнения программы, а нс во время компиляции. Ниже приводится программа, в которой демонстрируется использование виртуаль- ного .метода и его переопределенных версий. О В программе демонстрируется использование виртуального метода. fcs_ng System; --ass Base { ' Создание виртуального метода в наследуемом классе. '•'ublic virtual void who () { *4-------------| Объявление виртуального метода^ Console.WriteLine("Метод who(), определенный э классе Base."); Jss Derived : Base { Переопределение метода who() в наследующем классе Derivedl.___________ nublic override void who () { —-------———-——[Переопределение виртуального метиш.] Console.WriteLine("Метод who(), переопределенный в классе Derivedl.’1); L.ass Derived2 : Base { // Еще одно переопределение метода who О в наследующем классе Derivedz.
318 В каждом случае вызываемая версия метода who О определяется во время выполнения программы и зависит от типа объекта, на который ссыла- ется переменная baseHef. public override void who() { -*---------------------[Переопределение виртуального ме Console .WriteLine ("Метод who(), переопределенный в классе Derived2 .! publ Dcrivedl dObl - new Derived!(); Derived2 dOb2 - new Derived2(); Base baseRef; // Объявление ссылочной переменной тина наследуемого класса baseRef = baseOb; baseRet.who(); baseRef = dOb2; baseRef.who(); baseRef dObl; baseRef.who (); I Результат выполнения программы: Метод who(), определенный в классе Base. Метод who(), переопределенный в классе Derived!. Метод who(), переопределенный в классе Derived2. В этой программе создается наследуемый класс Base, а также два наследующих клаф Derivedl и Derived2. В классе Base объявляется метод who (), который затш переопределяется в его наследующих классах. В методе Маш () объявляются объект* имеющие тип Base, Derivedl и Derived2, а также объявляется ссылочная переме!- ная baseRef типа Base. Затем ссылочной переменной baseRef поочередно присваг ваются ссылки на объекты всех трех типов. Эти ссылки потом используются nW вызове метода who <). Как видно из результата выполнения программы, выбор версф вызываемого метода who () зависит от типа объекта, на который ссылается перемейг ная baseRef, а не от типа самой переменной. ф Переопределять виртуальный метод не обязательно. Если наследующий класс предоставляет собственную версию виртуального метода, то используется метф наследуемого класса. Например, /х Если не переопределен виртуальный метод, используется метод наследуемого1' using Sys terr.; class Base { // Создание виртуального метода в наследуемом классе. public virtual void who() { Console . WriteLine (’’Метод who(), определенный в классе Base."); class Derivedl ; Base {
виртуальные методы и переопределение 319 I переопределение метода who() в наследующем классе - public override void who() { Console.WriteLine("Метод who () , переопределенный в классе Derivedl."); Derived2 : Base { ◄—— -------[ В этом классе не переопределяется метод who () . В этом классе не переопределяется метод who(). class // class NoOveггideDemo { public static void MainO { Base baseOb - new Base(); Derivedl dObl “ new Derivedl0; Derived2 dOb2 new Derived2О; Base baseRef; /l Ссылочная переменная типа наследуемого класса. baseRef = baseOb; baseRe f.who() ; baseRef - dObl; baseRef . who () ; | Вызов метода who () класса Base, | baseRef = dOb2; baseRef.who(); // Вызов метода who(), который был определен в классе Base. Результат выполнения программы: Метод who{), определенный в классе Base. Метод who(), переопределенный в классе Derivedl. Метод who О, определенный в классе Base. Здесь в классе Derived2 не переопределяется метод who О, а при вызове метода wh- () объекта типа Derived2 вызывается метод who(), который был определен в классе Base. Для чего нужны переопределенные методы Переопределенные методы обеспечивают в C# поддержку полиморфизма во время выполнения программы. Полиморфизм очень важная составляющая объектно-ори- ентированного программирования, позволяющая определять в наследуемом классе Четоды, которые будут общими для всех наследующих классов, при этом наследую- щий класс может определять специфическую реализацию некоторых или всех этих '’еюдов. Переопределение методов представляет еще один способ реализации в C# пРинципа полиморфизма «один интерфейс, несколько методов». Чюбы успешно использовать полиморфизм, вы должны понимать, что наследующий 11 наследуемый классы формируют иерархию, в которой осуществляется переход от ''еньшей специализации к большей. В наследуемом классе определены члены класса, которые Которые Изменен могут непосредственно использоваться наследующим классом, и методы, в наследующем классе могут быть либо переопределены, либо оставлены без ия. В результате наследующий класс получает определенную свободу при определении собственных версий методов, сохраняя при этом совместимый интерфейс
320 Глава 8 Наследован^ (ю ешь возможность обращения к версии метода с использованием одного и то имени и одной и той же ссылочной переменной). Таким образом, применяя наслс лованис и переопределение методов в наследуемом классе, вы можете определят общие формы (операторы и переменные) методов, которые могут использовать^ всеми его наследующими классами. '/Z | у Вопрос. Могут ли свойства быть виртуальными? * Ответ. Ла. Свойства могут 6i.ni, объявлены с ключевым еловом virtdal,^ татем переопределены с указанием ключевого слова отегг.зе. Это справедливо j для индекса горов. Применение виртуальных методов Продолжая изучать виртуальные методы, используем их в классе TwoDShape. В пры дыдуших программах для каждого класса, наследующего класс TwcPShape, спреды лялся метод агеа(|. Зная материал предыдущего раздела, мы можем усовершенсп вовать программу, определив в классе TwoDShape виртуальный метод area (). Эпи метод можно переопределять в каждом наследующем классе, что дает возможносп вычислять площадь в зависимости от типа геометрической фигуры (треугольник квадрат и т. д.), информация о которой инкапсулирована в данном классе. (Дл| наглядности можно добавить в класс TwoDShape переменную name типа string, эт| позволит отображать название каждой геометрической фигуры.) Ниже приведен к<у> этой программы. ! Использование виртуальных методов и полиморфизма. class TwoDShape { double pri width; double prl_height; string pre name; /i Объявление закрытых переменных. /: Определение конструктора по умолчанию, public TwoDShape () { w_dth height С.С; пас.е "нуль"; } // Конструктор с параметрами. public TwoDShape(double w, double h, string n) { width - w; height =- h; name - n; / Конструктор, предназначенный для создания объектов, ' width и height присваивается одно и то же значение. public TwoDShape(double х, string n) • width - height - x; name •• n; у которых свойствам
^иРтУальнЫе метОДЬ1 и переопределение 321 Конструирование объекта на основе другого объекта. ;О_1с TwoDShape(TwoDShape ob) { width ob.width; height - ob.height; narne =* ob.name; i Определение свойств width, public double width { get { return pri__width; } c-et [ pri_width - value; } height и name. public double height ( get { return pri_height; } set { pri__height = value; } } public string name { get { return pri__name; } set { pri_name = value; } public void showDimO { Console.WriteLine("Значения ширины и высоты геометрической фих'уры = ” + width + ” и " + height); ___________________________________________ Метод area () определен в классе TwoDShape как виртуальный. punlic virtual double area() { Console. WriteLine ("Этот метод переопределяется в наследующих классах.’’); return 0.0; >- В этом классе, наследующем класс TwoDShape, определены характеристики, • • присущие треугольникам. class Triangle : TwoDShape { string style; // Закрытая переменная. Определение конструктора по умолчанию. Public Triangle () { style - "нуль"; Конструктор класса Triangle. Public Triangle(string s, double w, double h) base(w, h, "треугольник") ( style = s; f/ Конструктор, предназначенный для создания объекта, в котором определены // характеристики, присущие равнобедренным треугольникам. public Triangle(double х) : base(х, "треугольник") ( style = "равнобедренный ”;
322 Глава 8. Наследов; /,’ Конструирование объекта на основе объекта. public Triangle(Triangle ob) : base(ob) { style ob.style; // Переопределение метода area О в классе Triangle.__________________ public override double areaO { <------ГПсрсопределение метода area () в классе Trian^ return width x height / 2; public void showStyle() { Console.WriteLine{"Вид треугольника ” + style); // Класс Rectangle, наследующий класс TwoDShape. class Rectangle : TwoDShape { < // Конструктор, предназначенный для создания объекта, в котором определен // характеристики, присущие прямоугольникам, public Rectangle(double w, double h) : base(w, h, "прямоугольник"){ ; // Конструктор, предназначенный для создания объекта, в котором определен // характеристики, присущие квадратам. public Rectangle(double х) : base(x, "квадрат") { } : // Конструирование объекта на основе другого объекта. public Rectangle(Rectangle ob) : base(ob) i } I public bool isSquaref) { I if (width height) return true; | return false; } f J, !/ Переопределение метода area() в классе Rectangle. public override double area() { ц----------—--------— Переопределение метода area(F return width * height; в классе Rectangle- T class DynShapes public static { void Main() TwoDShape[1 shapes - new TwoDShape[5]; shapes[C] shapes [ 1 ] shapes[2] shapes[3; shapes[4] new Triangle("прямоугольный", 8.0, 12.0); new Rectangle(10); new Rectangle(10, 4); new Triangle(7.0); new TwoDShape(10, 20, "базовая"); for(int i-0; i < shapes.Length; 1++) ( Console. WriteLine (’’Эта геометрическая фигура shapes[i}.name); называется
323 ритуальные методы и переопределение Console.WriteLine("Площадь shapes[1]area() Console.WriteLine() ; Для каждой фигуры (каждого объекта) визы вас 1 ся соотнетс t вуюший ме тод рез\ лиат выполнения программы: Эта геометрическая фигура называется - 24,5 треугольник Эгта • еометрическая фигура называется Разовая Эта метод переопределяется в наследующих классах. Плс^г.дь - О Теперь рассмотрим программу подробнее. Как говорилось ранее, сначала в классе ’.« ' Shape объявляется метод area (.) с модификатором virtual, затем он переоп- ределяется в классах Triangle и Rectoiigle. Метод area О в классе TwoDShape не содержит операторов, предназначенных для вычисления площади геометрической фигуры. В результате его выполнения просто выводится информация о том, что этот чеюд должен быть переопределен в наследующих классах, в которых он действитель- но юлжен будет производить вычисления. Поэтому при каждом переопределении мен>да area () в нем определяется оператор, подходящий для вычисления площади геометрической фигуры, характеристики которой инкапсулированы данным насле- дующим классом. Таким образом, в случае создания класса Ellipse (эллипс), метод ю -д о должен будет вычислять площадь эллипса. Обратите внимание, что в методе МзгпО объявлен массив shapes, содержащий обьскты класса TwoDShape, но элементам данного массива присваиваются ссылки на объекты типа Trianqle, rectangle и TwoDShape. Эта правильно, поскольку сс ылочные переменные тина наследуемого класса (которыми в данном случае явля- мщя элементы массива) могут ссылаться на объекты наследующего класса. Затем программа обрабатывает в цикле элементы массива, отображая сведения о каждом 0|ч,сктс. Хотя эта программа достаточно проста, она демонстрирует возможности "‘•следования и переопределения методов. Тип объекта, ссылка на который хранится 11 ‘Сылочной переменной, имеющей тип наследуемого класса, определяется во время "'•полнения программы, тогда же вызывается соответствующий метод. Если объект следует класс TwoDShape, площадь геометрической фигуры вычисляется с помо- ю метода area (). Интерфейс этой операции одинаков для всех типов используе- 'И фигуры (объекта).
324 Глава 8. Наследо, Я? Минутный практикум №«' $ ’®, 1. Что такое виртуальный метод? Как он переопределяется? - Какой принцип C# поддерживают виртуальные методы? 3. Какая версия виртуального метода вызывается, сели сам метод вызва помощью ссылочной переменной типа наследуемого класса? । Использование абстрактных классов Иногда требуется создавать наследуемый! класс, в котором определены лишь нец рые характеристики методов, такие как тип возвращаемого значения, имя и спи параметров. Тела этих методов пустые, поскольку трудно предвидеть, какие пере; ныс и операторы могут понадобиться в объектах наследующих классов. Поэтси наследующем классе программист должен реализовать эти методы — опредц переменные и операторы, которые будут выполнять действия, необходимые объекта этого класса. То есть в таком классе определяются лишь общие пред на чения методов, которые должны быть реализованы в наследующих классах, но по себе этот класс не реализует один или несколько подобных методов. Напри это происходит при использовании класса TwoDShape, рассмотренного в предыду разделе. Определение метода area () в классе TwoDShape представляет собой тот «каркас», и при его выполнении ничего не вычисляется. При создании библиотек классов вы, наверное, на собственном опыте убедили том, что часто возникает ситуация, когда в наследуемом классе невозможно пре; деть действия, которые должен будет выполнять метод в наследующих клас В базовом классе метод можно определить лишь частично (например, в кл< TwoDShape для метода area () выводится предупреждающее сообщение о том, его нужно переопределить, чтобы он действительно возвращал значение площ фигуры). Но иногда, создав достаточно большую библиотеку классов или работ? встроенными классами, программист вынужден (чтобы вспомнить нужную инф мацию либо сэкономить время) просматривать код класса или список членов кла, В этих списках приводятся лишь подписи методов — имена методов, типы возв шаемых значений, списки параметров. Если требуется наследовать этот класс, такой информации недостаточно для понимания того, определен метод полност или частично. Поэтому для более эффективного использования наследования базовом классе можно определить абстрактные методы, которые имеют пустые те; При объявлении абстрактного метода используется модификатор abstract, поэтоь встретив в списке членов класса метод с этим модификатором, программист зна< что он обязан реализовать (определить) этот метод в наследующем классе. Абстрат ный метод автоматически становится виртуальным, так что модификатор virtu при объявлении такого метода не нужен. Более того, совместное использован модификаторов virtual и abstract приведет к возникновению ошибки. 1. Виртуальным называется метод, объявленный в наследуемом классе с помощью ключе® слова virtual и переопределенный в наследующем классе. Виртуальный метод переоп] деляется с указанием модификатора override. 2. Виртуальные методы являются одним из способов поддержки полиморфизма в С#. 3. Вызываемая версия переопределенного метода определяется исходя из типа объекта, который ссылается переменная во время вызова.
^пользование абстрактных классов 325 f1pn объявлении абстрактного метода используется следующая форма синтаксиса: rract type name (parameter-list) ; g этом объявлении отсутствует тело метода. Модификатор abstract может исполь- зоваться только с обычными методами и не может указываться для методов, имеющих тип s зГ*с‘ (Спасе, содержащий один или более методов, должен быть объявлен как абстрактный (то сеть при его объявлении указывается модификатор abstract). Поскольку абст- рактный класс не определен полностью, объекты этого класса создать невозможно, и попытка создания объекта абстрактного класса с помощью ключевого слова new приведет к ошибке компиляции. Когда класс наследует абстрактный класс, он должен реализовать все абстрактные методы наследуемого класса. Если этого не происходит, то наследующий класс также должен быть объявлен с модификатором abstract. Таким образом, атрибут ab- stract наследуется до тех пор, пока методы, а значит и сам класс, не будут полностью реализованы. Продолжим модификацию класса TwoDShape и объявим его как абстрактный. В этом классе отсутствует реализация метода агеа(), предназначенного для вычисления площади неопределенной двухмерной геометрической фигуры. Поэтому в версии программы, которая рассматривалась в предыдущем разделе, метод a real) класса TwoDShape определяется как абстрактный, а сам класс TwoDShape объявляется с модификатором abstract. Таким образом, наследование всеми классами класса TwoDShape означает, что в них должен быть переопределен метод area (). II Создание абстрактного класса. using System; abstract class TwoDShape { 4________j Класс TwoDShape объявляется с модификатором abstract. douole pri_width; // Объявление закрытых переменных. douole pri_height; string pti nar.e; />' Определение конструктора по умолчанию. Public TwoDShape() { width - height - 0.0; name — "нуль"; ' Конструктор с параметрами. Public TwoDShape(double w, double h, string n) width - w; height = h; name r.; '- конструктор, предназначенный для создания объектов, у которых свойствам 1' width и height присваивается одно и то же значение. P-b-ic TwoDShape(double х, string n) ( width - height - x; name - n;
326 Глава 8. Наследсц // Конструирование объекта на основе объекта, public TwoDShape(TwoDShape ob) { width -= ob.wxdth; height - ob. height; name - ob.name; // Определение свойств width, height и name, public double width { get { return pri width; } set t pri__width -- value; } } public double height { get { return pri__height; } set { pri_height — value; } } public string name { get { return pri_name; } set { pri _name - value; } public void showDim() • Console.WriteLine("Значения ширины и высоты геометрической фигуры - ” + width + " и " 4 height); // Теперь метод public abstract area() будет абстрактным. double a re а О ; -4--——— Объявление абстрактного метода area О в классе TwoDShape. // В этом классе, наследующем класс TwoDShape, определены характеристики, // присущие треугольникам. class Triangle : TwoDShape ( string style; // Закрытая переменная. // Определение конструктора ио умолчанию, public Triangle() { style "нуль"; } // Конструктор класса Triangle. public Triangle(string s, double w, double h) : ease(w, h, "triangle") t style - s; // Конструктор, предназначенный для создания объекта, в котором определены // характеристики, присущие равнобедренным треугольникам. public Triangle(double х) : base(x, "треугольник") { style = "равнобедренный"; // Конструирование объекта на основе объекта, public Triangle(Triangle ob) : basc(ob) {
^пользование абстрактных классов 327 jе р е о 11 р е д е j i е ни с- метода а г е а () о классе Triangle.____ override douole area О { **----If! epcoi i редел ен ие меч ода area () в классе Triangle. с tarn width * height / 2; _ic void showStyle () { • msole Wri teLine ("Вид треугольника - " r style); /, r ..i.. Rectangle, наследующий клас. TwoDShape. ,< а-Rectangle : TwoDShape • Конструктор, предназначенный для создания объекта, в котором определены ,, характеристики, присущие прямоугольникам. rue Lie Rectangle(double w, double h) : case(w, h, ’'прямоугольник”) { } i Конструктор, предназначенный для создания объекта, в котором определены /, характеристики, присущие квадратам. purlic Rectangle(double х) : Ocise (х, "квадрат") { } Конструирование объекта на основе объекта. public Rectangle(Rectangle ob) : base(ob) { } : .olic bool isSguareC) { (width -— height) return true; return false; Переопределение метода area() в классе Rectangle. f..olic override double area() { -< return width * height; Переопределение метода area () в классе Rectangle () . • tss AbsShape { viblic static void Main() { TwoDShape[] shapes - new TwoDShape[4]; shapes [0] =- new Triangle ("прямоугольный", 8.0, 12.0); shapes[1] new Rectangle(10); shapes[2] new Rectangle(10, 4); shapes[3] - new Triangle(7.0); tor (int 1^0; i < shapes. Length; i-r-) i Console.WriteLine ("Эта фигура называется - " ‘ shapes[1].name); Console. WriteLine ("Площадь - " + shapes.ij’.areaO); Console.WriteLine();
328 Глава 8. Наслвдов^ИЕ Как видите, все наследующие классы должны либо переопределять метод area (), быть объявленными с модификатором abstract. Чтобы проверить это, попытай^М создать наследующий класс, не переопределяющий метод агеа(). В результате буДГ выведено сообщение об ошибке компиляции. Конечно, остается возможность создай® ссылочной переменной типа TwoDShape, что и реализовано в программе, но объявлен! объектов, имеющих тип TwoDShape, теперь невозможно. Поэтому в данной верЖ программы объявляется массив из четырех элементов, а не из пяти, как в предыдуцЖ И последнее замечание, в класс TwoDShape по-прежнему включен метод showDimjr- который не модифицирован с помощью киочевого слова abstract. В абстракта® классе могут присутствовать полностью определенные методы, которые могут Ж пользоваться наследующими классами без переопределения. В наследующих классах должны быть переопределены только абстрактные методы. Минутный практикум 1. Что такое абстрактный метод? Как он создается? 2. Что такое абстрактный! класс? 3. Можно ли создавать объекты абстрактного класса? Использование ключевого слова sealed с целью предотвращения наследования Каким бы полезным и мощным ни было наследование, иногда возникает необходи- мость в создании таких классов, которые наследовать невозможно. Например, если требуется класс, в котором инкапсулирована последовательность инициализации некоторых специализированных аппаратных средств, то нежелательно, чтобы поль- зователи класса имели возможность изменять способ инициализации монитора, поскольку в противном случае устройство может быть настроено неправильно. В C# можно легко предотвратить возможность наследования класса с помощью ключевого слова sealed, которое указывается в начале объявления класса. Невоз- можно объявить класс, указав одновременно оба ключевых слова, abstract и sealed, поскольку абстрактный класс является незавершенным и для реализации своих абст- рактных методов и возможности создания объектов должен быть наследован. Пример класса, объявленного с использованием ключевого слова sealed: sealed class А { •<-----Этот класс не может быть наследуемым. // ----------------------------------------------- ) // Это объявление класса недействительно. class В : А {// ОШИБКА! Невозможно наследовать класс А // } Как следует из комментариев, класс В нс может наследовать класс А, если класс А объявлен как sealed. 1. Абстрактным называется метод без тела, при создании которого указывается ключевое слово abstract, тип возвращаемого значения, имя метода и список параметров. 2. Абстрактным называется класс, который содержит как минимум один абстрактный метод. 3. Нет.
^acc object 329 jjqcc object g С-г определен специальный класс object, который является базовым для всех руг их классов и для всех других типов (включая обычные типы). Другими словами, еСе швы наследуют класс object, то есть ссылочная переменная типа object может ссЫ-'‘,| ься иа объект любого другого типа. Также эта переменная может ссылаться на .цобои массив, поскольку массивы реализованы в C# как классы. Имя object — это Кра1кая форма имени объекта System.Object, который является составной частью ।потеки классов .NET Framework. g классе object определены следующие доступные для каждого объекта методы: Метод public virtual bool Equals(object object} public static bool Equals(object 0b1, object Ob2} protected Finalyze public virtual int GetHashCode() public Type GetTypeO protected object MemberwiseClone() public static bool ReferenceEquals (object 0b1, object 0b2} P'-ulic virtual string ToStringO Назначение Определяет, являются ли вызывающий объект и объект, передаваемый в качестве параметра object, одинаковыми Определяет, являются ли одинаковыми объекты оЫ и оЬ2, переданные методу в качестве параметров Выполняет действия, связанные с завер- шением выполнения программы, до «сборки мусора». В C# доступ к методу Finalize осуществляется с помощью де- структора Возвращает хеш-код, ассоциированный с вызывающим объектом Получает тип объекта во время выполне- ния программы Создает «мелкую копию» объекта, в кото- рую копируются члены класса, но при этом не копируются объекты, на которые ссыла- ются эти члены класса Определяет, ссылаются ли ссылочные пе- ременные оЬ1м оЬ2на один и тот же объект Возвращает строку, описывающую объект Описание назначения некоторых из указанных методов нуждается в дополнительных Пояснениях. Метод Equals (object) определяет, ссылается ли ссылочная перемен- ная типа вызывающего объекта на тот же самый объект, что и ссылочная переменная, Переданная в качестве аргумента (при этом определяется, будут ли совпадать две педлки). При выполнении метода возвращается значение true, если это один и тот объект, и значение false в противном случае. Этот метод может быть переопре- Пе юн при создании пользовательских классов с указанием критериев, по которым °)дет оцениваться равенство объектов. Например, можно создать метод Equals (ob- ct) , который сравнивает содержимое двух объектов. Метод Equals (object, ]ect) для сравнения объектов, передаваемых в качестве параметров, вызывает Нетод Equals(object). Метод GetHashCode () возвращает хеш-код, ассоциированный с вызывающим объ- ектом. Этот код может использоваться произвольным алгоритмом, применяющим Ьгширование в качестве средства доступа к хранимым объектам.
330 Глава 8. Наследован, В главе 7 уже говорилось, что для псрорузкн оператора - обычно приходите переопределять методы -ч 1 :s г) и лст I!чзЬСоН" <:1 , поскольку в болыпинстЛ случаев нужно, чтобы этот оператор и меюды EgoaLsO функционировали одинЛ ково. В случае переопределения метода Equals (; необходимо также переопределит! метод GetHashCccte () для достижения их совмссiимости. Я Метод ToStringt) возвращает строку, содержащую описание объекта, из которого он вызывается. При указании объекта в качестве параметра метода WriteLine (J метод ToStringO вызывается автоматически. Во многих классах этот метод можб| быть переопределен, благодаря чему будет отображаться информация, относящая^! к каждому конкретному объекту данною класса. Например, 3 // В программе демонстрируется кскользонание метода ГооЬг_пд () . 1 using System; class MyClass { static int count int id; public MyClass() id - count; count-гт; } public override string ToStringO « return "Объект типа MyClass № class Test { public static void Main() { MyClass obi - new MyClassО; MyClass ob2 =- new MyClass О ; MyClass ob3 new MyClass (); Console.WriteLine(obi); Console.WriteLine(ob2); Console.WriteLine(obi); A Результат выполнения программы: Объект тика MyClass У 1 Объект типа MyClass N' 2 Объект типа MyClass N' 3 Упаковка и распаковка Ранее уже говорилось о том, что вес типы С#, включая обычные типы, наследуют,; класс object. Поэтому ссылочная переменная типа object может ссылаться на | объект любого другого типа, включая обычные типы. При присваивании ссылочной | переменной типа object значения обычного типа происходит процесс, называемый | упаковкой, в результате выполнения которого значение будет храниться в объекте V
331 упаковка и распаковка (Гш obi ect. То есть значение обычного типа будет «упаковано» внутри объекта. Затем этот объект может использоваться как любой другой! объект. Упаковка выполняется соматически, для этого нужно просто присвоить значение обычного типа ссылочной переменной типа object, остальную работу выполнит компилятор языка С#. g процессе распаковки происходит извлечение значения из объекта. Для осушсств- 11?Н11Я этого процесса требуется выполнение операции приведения типа. При при- енаивании обычной переменной значения, извлекаемого из объема, перед ссылочной переменной типа object в скобках указывается тип, к которому должно быть приведено распаковываемое значение. Ниже приводится простая программа, в которой выполняется упаковка и распаковка значения. // в программе демонстрируется использование упаковки и распаковки. using System; class BoxingDemo { puL'_ic static void Main (J ( i n t x ; inject obj ; J При выполнении этого оператора происходит упаковка значения переменной х в объект. х - 10; c-joj — х; // Упаковка значения переменной х в объект. int у ~ (int)obj; // Распаковка (извлечение) значения, хранящегося в // объекте obj, и присваивание его переменной типа int. Console.WriteLine (у) ; . При выполнении этого оператора происходит распаковка значения. При выполнении этой программы будет выведено значение 10. Обратите внимание, что упаковка значения переменной х выполняется в результате присваивания этого значения ссылочной переменной obj, имеющей run object. Целочисленное значение извлекается из переменной obj при помощи выполнения операции приведения типа. Ниже приводится еще один пример использования упаковки. В программе значение типа int передается в качестве аргумента методу sqr(), которому должен переда- ча! вся параметр типа object. •> программе демонстрируется выполнение упаковки при передаче методу значений в качестве аргументов. L^:nq System; c-.iss BoxingDemo { -•cblic static void Main() { x - 10; Console.WriteLine("Значение переменной x ^ " + x) ; // Значение переменной x автоматически упаковывается при передаче его / методу sqr () . Значение переменной х упаковывается при х BoxingDemo. sqr (х) ; <----—------- вызове метода sqr (). __________ Console. WriteLine ("Значение переменной х во второй степени - " +• х) ;
332 Глава 8. Наследоа, static int sqr(object о) { ; return (int)о * (int)о; “ } j В результате выполнения программы будут выведены следующие строки: Значение переменной х = 10 Значение переменной х во второй степени = 100 ; Значение переменной х автоматически упаковывается при передаче его методу sqrJ Упаковка и распаковка обеспечивают полную унификацию системы типов. Все ти| наследуются из класса object. Ссылка на любой тип может быть присвоена ссылочи переменной. Упаковка/распаковка значений обычных типов выполняется автоматик ски. Кроме того, поскольку все типы наследуют класс obj ect, они имеют доступ к метод класса object. В качестве примера приведем несколько неожиданную программу. // При использовании упаковки можно вызывать методы значения так же, как // обычно вызывают методы экземпляра! using System; class MethOnValue { public static void Main() { Такой оператор является действительным в С#! Console.WriteLine(10.ToString()) ; t -i 4 В результате выполнения программы отображается строка, состоящая из двух сим- волов 10. Это происходит в результате того, что метод ToString () возвращает строковое представление объекта, из которого он вызывается. В данном случае строковым представлением значения 10, принадлежащего к типу int, будет строк|, состоящая из двух символов 10. я Минутный прктикум ; 1. Если класс объявлен с модификатором sealed, может ли он быть наследуе- мым? 5 2. Что представляет собой клас object? ? 3. Как предотвратить возможность наследования класса? 1. Нет. 2. Класс object — это базовый класс для всех типов, определенных в С#. 3. Для предотвращения возможности наследования класс должен быть объявлен с модифика- тором sealed.
^оНтрольные вопросы ззз Контрольные вопросы , 1. Имеет ли наследуемый класс доступ к членам наследующего класса? I Имеет ли наследующий класс доступ к членам наследуемого класса? I I 2. Создайте наследующий класс класса TwoDShape и назовите его Circle. | Включите в его определение метод агеа(). предназначенный для вы- ] числения площади окружности, а также конструктор, использующий | ключевое слово base для инициализации той части объекта, которая | была наследована от класса TwoDShape. | 1 3. Каким образом можно предотвратить доступ членов наследующего j класса к членам наследуемого класса? I 4. Укажите назначение ключевого слова base. I 5. Для следующей иерархии наследования укажите порядок, в котором | вызываются конструкторы классов при создании объекта класса Gamma, j class Alpha { ... * class Beta : Alpha t ... 5 class Gamma : Beta { .. j 6. Ссылка наследуемого класса может указывать на объект наследующего ( класса. Объясните, почему это имеет значение при переопределении I метода. j 7. Что такое абстрактный класс? [ 8. Каким образом можно предотвратить возможность наследования класса? 9. Объясните, каким образом при использовании наследования, переоп- ределения методов и абстрактных классов поддерживается полимор- н физм. ' К I 10. Какой класс является базовым для всех остальных классов? * 11. Объясните суть процесса упаковки. 12. Каким образом осуществляется доступ к членам класса, объявленным с модификатором protected?
Интерфейсы, структуры и перечисления □ Использование интерфейсов □ Применение ссылок на интерфейсы □ Добавление в интерфейсы свойств и индексаторов □ Наследование интерфейсов □ Использование явных реализаций □ Исследование структуры □ Применение перечислений
335 ^терФ£^ наСтояшсй главе рассматривается один из наиболее важных элементов С#: интер- . Этот элемент определяет набор методов, которые могут реализовываться с мошью класса. Конечно, интерфейс сам по себе нс может реализовывать методы, является чисто логической конструкцией, которая описывает набор поддержи- н\ классом методов, не диктуя при этом способ реализации. g ri;njC также обсуждаются два новых типа данных, поддерживаемых в СТ структуры и перечисления. Структуры напоминают классы, отличаясь от последних тем, что Moryi обрабатывать типы значений, а не ссылочные типы. Перечисления представ- 1ЯЮТ собой перечни именованных целочисленных констант. Благодаря этим новым типам данных значительно расширяются возможности среды программирования С#. Интерфейсы В объектно-ориентированном программировании иногда возникает необходимость определения действий!, выполняемых классом, без указания способа выполнения этих действий. Ранее уже рассматривался подобный пример: абстрактный метод. Абст- рактный тип метода определяет сигнатуру метода, но нс поддерживает реализацию. В этом случае наследующий класс должен поддерживать свою собственную реализ- ацию для каждого абстрактного метода, определенного его наследуемым классом. Таким образом, абстрактный метод определяет интерфейс метода, но не его реализа- цию. Исходя из полезности абстрактных классов и методов, имеет смысл расширить рассматриваемую концепцию. В C# можно полностью отделить интерфейс класса от реализации этого класса. В этом случае используется ключевое слово interface. Синтаксис интерфейсов подобен синтаксису абстрактных классов. Однако в случае интерфейсов методы не могут включать тело. Таким образом, интерфейсы ни при каких условиях не поддерживают реализацию. Этот объект определяет, что нужно делать, но не демонстрирует, как именно это нужно делать. Сразу же после того, как интерфейс определен, он может реализовываться произвольным количеством классов. Один класс, в свою очередь, может реализовывать любое число интерфейсов. Для выполнения реализации интерфейса класс должен поддерживать тела (реализа- ции) д.1я методов, описываемых с помощью данного интерфейса. Каждый класс может свободно определять детали своей реализации. В этом случае два класса могут Реализовывать один и тот же интерфейс различными способами, но каждый отдель- ный класс по-прежнему поддерживает один и тот же набор методов. Таким образом, К°Д, который «знает» о наличии интерфейса, может использовать объекты любого Класса в случае, если интерфейс для этих объектов будет одинаковым. Путем п°4лержки интерфейса C# позволяет полностью реализовать аспект полиморфизма, НМСЩСМЫЙ «один интерфейс, несколько методов». Объявление интерфейсов выполняется с помощью ключевого слова interface, нжс приводится упрощенная форма объявления интерфейса: * I Н Го П rirriri J type type - type method-namel (param-list) ; method-name2(param-list); method-nameN (param-list) ;
336 Глава 9. Интерфейсы, структуры и перечи. Имя интерфейса указывается с помощью параметра name. Объявление методД реализуется с помощью указания их сигнатур и типа возврата. Фактически в даннЛ случае речь идет об абстрактных методах. Ранее уже упоминалось о том, чтоД интерфейсе, определенном с помощью ключевого слова interface, методы не могЗ иметь реализацию. Следовательно, каждый класс, включающий interface, дод^Я реализовывать все эти методы. В интерфейсе для методов неявным образом задается тип public. В этом случае также не допускается явный спецификатор доступа. 3 Ниже приводится пример объекта interface. Он определяет интерфейс для классе который генерирует наборы чисел. public interface Series ( J int getNextf); // Возврат следующего числа в наборе. 'Я void resetf); // Перезагрузка. ч void setstart (int х) ; // Установка начального значения. -1 } । При объявлении этого интерфейса используется модификатор public, поэтому cri может реализовываться с помощью любого класса в любой программе. ] В дополнение к объявлению сигнатур методов интерфейсы могут объявлять сигнад туры свойств, индексаторов и событий. События будут описываться в главе 10, сейчас вкратце рассмотрим методы, свойства и индексаторы. Интерфейсы не могуч включать данные-члены. Они также не могут определять конструкторы, деструктора или операторные методы. Кроме того, никакой из членов не может определяться ! виде static. Я Реализация интерфейсов J Как только interface был определен, он может быть реализован одним иля несколькими классами. Для реализации интерфейса после имени класса необходиьЙ указать название интерфейса (почти так же, как и в случае с определением наследуем мого класса). Ниже приведена общая форма синтаксиса кода класса, реализующее^ интерфейс: class class-name: interface-name ( 11 тело класса ' j } 1 Имя реализуемого интерфейса указывается с помощью параметра interface-name.® Если класс реализует интерфейс, он обязан реализовать его в целом. Например;» невозможно указать и реализовать только часть интерфейса. Классы могут реализовывать более одного интерфейса. В случае подобной реализаций^ интерфейсы должны разделяться запятыми. Класс может наследовать наследуемый, класс, а также реализовывать один или больше интерфейсов. В этом случае в списке,,’ разделенном запятыми, имя наследуемого класса должно быть первым. • Методы, реализующие интерфейс, следует объявлять как public. Это необходимо.' делать по той причине, что методы неявным образом общедоступны внутри интер- фейса, поэтому их реализации также должны быть общедоступны. При этом тип сигнатуры реализуемого метода должен точно соответствовать типу сигнатуры, ука- занному в определении interface.
у1нтерФейсы 3: Циже приводится пример, в котором реализуется (с помощью класса ByTwc указанный ранее интерфейс Series. Этот класс позволяет генерировать наб чисел, причем каждое из последующих чисел на 2 больше, чем предыдущее. // с- Реализация класса Series ass ByTwos : Series ( Реализация интерфейса Series. ,nt start; int val; public ByTwos0 { start = 0; val = 0; public int getNextO ( val += 2; return val; public void reset!) { start = 0; val = 0; public void setstart(int x) ( start = x; val = x; } Нетрудно заметить, что интерфейс ByTwos реализует все три метода, определенн с помощью интерфейса Series. Это пришлось сделать потому, что класс не моя выполнить частичную реализацию интерфейса. Ниже приводится код, демонстрирующий возможности класса ByTwos: // Демонстрация возможностей класса ByTwos. using System; class SeriesDemo ( public static void Main!) ( ByTwos ob = new ByTwos(); for(int i=0; i < 5; i++) Console.WriteLine("Следующее значение " + ob.getNext()); Console.WriteLine("\пВосстановление"); ob. reset {) ; for(int i=0; i < 5; i++) Console.WriteLine("Следующее значение " + ob.getNext()); Console.WriteLine{"\пНачало co 100"); ob.setStart(100); for(int i=0; i < 5; i++) Console.WriteLine("Следующее значение " + ob.getNext () ) ;
। лава э. Интерфейсы, структуры и перечисле. С целью компиляции класса SeriesDemo в процесс компиляции потребуется вклкм чить классы Series, ByTwos и SeriesDemo. В результате компилятор автоматически! скомпилирует все три файла и создаст конечный исполняемый файл. Например, если? данные файлы именуются Series . cs, ByTwos . cs и SeriesDemo . cs, для запуска цаг] выполнение процесса компиляции используется следующая командная строка: j >csc Series.cs ByTwos.cs SeriesDemo.es Если используется визуальная среда разработки Visual С+4- IDE, тогда просто! добавьте упомянутые три файла в проект С#. Помещение всех трех файлов в один и ' тот же файл вполне себя оправдывает. j Результат выполнения программы: Следующее значение 2 Следующее. значение 4 Следующее значение 6 Следующее значение 8 Следующее значение 10 Восстановление Следующее- Следующее Следующее Следующее Следующее значение 2 значение 4 значение 6 значение 8 значение 10 Начало со 100 Следующее значение 102 Следующее значение 104 Следующее значение 106 Следующее значение 108 Следующее значение 110 Классы, реализующие интерфейсы, могут определять дополнительные члены с целью их применения в своих собственных целях. Например, в следующей версии интер- фейса ByTwos добавляется метод getPrevious (), возвращающий предыдущее зна- чение: // Реализация интерфейса Series и добавление метода getPrevious(). class ByTwos : Series { int start; int val; int prev; public ByTwos() { start - C; val = 0; prev -2; 1 public int getNextO i prev = val; val += 2; return val;
интерфейсы 33 Метод, не указанный с помощью Series. getPrevious() { Ч---[Добавление метода, нс определенною с помощью ишерфсйса Scr.iZZT eturn prev; Обри гите внимание на то, что добавление метода getPrevious О потребует измене- ния реализаций методов, определенных с помощью интерфейса Series. Однако, поскольку интерфейс для этих методов остается неизменным, изменения будут несметны и нс отразятся па предшествующем коде. Это и является одним из преимуществ, обеспечиваемых интерфейсами. Как объяснялось ранее, любое количество классов может быть реализовано по- средством ключевого слова interface. Например, ниже приводится код класса В. -.tees, который генерирует наборы чисел, кратных трем: Реализация интерфейса Series ______________________________________ •..-,ss ByThrees : Series {<-----(реализация интерфейса Series другим способом, .nt start; -lit val; public ByThrees() { start = 0; val - 0; _.olic int getNextO { val 3; return val; put-lie vole reset () start 0; val - 0; t-ublic void setstart (int x) { start =- x; val — x ;
340 Глава 9. Интерфейсы, структуры и перечисле. Минутный практикум 1. Каково общее назначение интерфейса? 2. Какие элементы могут быть членами интерфейса? 3. Каким образом реализуются интерфейсы с помощью класса? Использование интерфейсных ссылок Вы, наверное, удивитесь, когда узнаете о том, что можно объявлять переменнуи ссылки, имеющую интерфейсный тип. Другими словами, можно создавать интер. фейсную переменную ссылки. Подобная переменная может ссылаться на любо! объект, который реализует ее интерфейс. При вызове метода объекта с помощьк интерфейсной ссылки вызывается версия метода, реализуемого данным объектом Этот процесс напоминает использование ссылки наследуемого класса для доступа j объекту наследующего класса, как описано в главе 8. В следующем примере иллюстрируется применение интерфейсной ссылки. Здес] задействована одна и та же интерфейсная переменная ссылки для вызова объектов ] интерфейсах ByTwos и ByThrees. // Демонстрация возможностей интерфейсных ссылок, using System; // Определение интерфейса public interface Series { int getNextf); // Возврат следующего числа из набора. void resett); // Перезапуск. void setStart(int х) ; // Установка начального значения. } // Реализация интерфейса Series одним способом. class ByTwos : Series { int start; int val; public ByTwos() ( start = 0; val = 0; public int getNextO ( val += 2; return val; } public void reset!) ( start = 0; val = 0; 1. Интерфейс определяет то, что должен выполнять класс, но не указывает конкретный алгоритм действий. 2. Членами интерфейса могут быть методы, свойства, индексаторы и события. 3. Для реализации интерфейса после имени класса укажите ключевое слово interface, затем выполните реализацию для каждого члена интерфейса.
использование интерфейсных ссылок 341 public void setstart(int x) ( start =- x; .al = x; I, .еализация интерфейса Series другим способом, rlass ByThrees : Series { start; int val; puolic ByThrees () { start - 0; val 0; public int getNextO ( val += 3; return val; public void reset() { start = 0; val = 0; public void setStart(int x) { start - x; val * x; class SeriesDemo2 { public static void Main() { ByTwos twoOb = new ByTwos(); ByThrees threeOb - new ByThrees(); Series ob; for(int i-0; i < 5; i++) { ob - twoOb; Console.WriteLine("Следующее значение ByTwos " + ob.getNext()); ob = threeOb; Console.WriteLine("Следующее значение ByThrees " + ob.getNext()); В методе Main о объект ob объявлен в качестве ссылки на интерфейс series. Это означает, что он может использоваться с целью хранения ссылок на любой объект, Реализующий интерфейс Series. В данном случае он используется для ссылки на обьекты twoOb и ThreeOb, которые являются объектами типа ByTwos и ByThrees соответственно, и оба они реализуют интерфейс Series. Интерфейс ссылочной переменной знает лишь о методах, объявленных в его разделе и указанных его ключевым словом interface. В результате объект ob не может использоваться при Доступе к любым другим переменным или методам, которые поддерживаются объектом.
342 Глава 9. Интерфейсы, структуры и перечисления Проект 9.1. Создание интерфейса Queue IcharQ.cs, IQDemo.cs Для того чтобы увидеть интерфейсы в действии, обратимся к практическому примеру, В предыдущих главах был разработан класс Queue, с помощью которого была реализована одиночная очередь для символов фиксированного размера. Однако реализовать очередь можно самыми различными способами. Например, очередь может иметь фиксированный размер, либо увеличиваться Очередь может быть линейной (в этом случае элементы находятся в очереди постоянно), либо круговой (в этом случае элементы могут быть помешены в очередь по мере удаления из нее старых элементов). Очередь также может помешаться в массиве, в связанном списке, в двоичном дереве и других подобных объектах. Каков бы ни был способ реализации очереди, ее интерфейс остается одним и тем же, а методы put () и get О определяют интерфейс независимо от деталей реализации. Поскольку интерфейс очереди отделен от ес реализации, довольно просто можно задать его, оставив на долю каждой , реализации определение Toil или иной специфической черты. В ходе осуществления данного проекта мы создадим интерфейс очереди символов, а также три его реализации. Во всех трех реализациях для хранения символов исполь- зуются массивы. Одна очередь будет линейной с фиксированным размером. В каче- стве второй очереди выступает круговая очередь (в этом случае по достижении конца базового массива методы get и set автоматически возвращаются к началу цикла). Таким образом, в круговой очереди может сохраняться любое количество элементов , до тех пор, пока возможно удаление из очереди ранее введенных в нее элементов. В последней реализации создается динамическая очередь, которая в случае превы- шения изначального размера может автоматически увеличиваться. Пошаговая инструкция I. Создайте класс iCharQ.cs и поместите его описание в следующее определение интерфейса: // Интерфейс очереди символов. public interface ICharQ I II Помещение символа в очередь, void put(char ch); // Выборка символа из очереди. char get (); } Как видите, данный интерфейс является достаточно простым и включает лишь два метода. Каждый класс, реализующий icharQ, нуждается в реализации упомя- нутых двух методов. 2. Создайте файл iQDemo.cs. 3. Начните создавать проект iQDemo.es, воспользовавшись указанным ниже кодом класса FixedQueue: /* Проект 9.1 Демонстрация возможностей интерфейса ICharQ. */
Использование интерфейсных ссылок 343 using System; Класс очереди фиксированного размера, предназначенный для выполнения операций с символами. class b’ixcdQueue : ICharQ { char- q; Массив, предназначенный для хранения очереди int putloc, getloc; // Индикаторы put и get ' Конструирование пустой очереди, имеющей заданный размер. puolic FixedQueue(int size) ; q - new char lsize-rl ] ; >!! Выделение памяти для очереди. putloc - getloc - С; Помещение символа в очередь. public void put(char ch) ( if (putloc---q.Length-1) ( Console.WriteLine(" Очередь заполнена.”); return; рнИостт; qtputloc] = ch; // Получение символа из очереди. public char get() { if(getloc ~ = putloc) { Console.WriteLine(" - Очередь пуста.’’); return (char) 0; } get1oct+; return q[getloc]; } Чанная реализация интерфейса ICharQ является адаптированной! версией класса . leue, рассматриваемого в главе 5, и поэтому должна быть вам знакома. * В файл rQDemo.es добавьте приведенный ниже кол класса CiicoiarQueue. Этот класс реализует круговую очередь для символов. ' Круговая очередь. class CircularQueue : ICharQ { char[] q; // Очередь содержится в этом массиве. int putloc, getloc; // Индикаторы put и get. // Пустая очередь заданного размера. public Circular-Queue (int size) i q = new char•size+1]; // Выделенке памяти для очереди. putloc getloc С; } // Помещение символа в очередп. puolic void put(char ch) { /* Очередь заполнена, либо если putloc меньше чем getloc, либо если putloc представляет конец массива, a getloc представляет начало массива. */
. „<ща и. интерфейсы, структуры и перечисли if (pu11 ос +1 --gc11 ос I ((putlcc-=q.Length-1) & (getloc—0))) ( /Иг Console . WriteLine (" - Очередь заполнена."); return; putloc++; ' ДУ if (putloc=-=q. Length) putloc - 0; // Обратный цикл. qlputloc] = ch; } Ж // Выборка символа из очереди. public char get() ( if (getloc -- putloc) ( Console . Wri teLine (" Очередь пуста."); return (char) G; Т» ж. getloc++; ’ a if (getloc==q.Length) getloc = 0; // Обратный цикл. яВ return q[getloc]; ;'/ Ж Ж При использовании круговой очереди может повторно задействоваться простран- ство массива, освободившееся после выборки элементов. В результате очереди подобного типа может последовательно включать неограниченное количество элементов, если происходит удаление из нее ранее помешенных элементов,' Концептуально все достаточно просто — нужно просто переустановить значений соответствующего индекса в ноль после достижения конца массива — но при этом; на первых порах остаются трудности с определением граничных условий. В про- цессе использования круговой очереди момент ее заполнения определяется не тогда, когда достигается конец базового массива, а в том случае, если при1 сохранении элемента происходит переписывание невыбранного элемента. При этом метод put () должен проверить несколько условий с целью определения того, является ли очередь заполненной. Как и предполагается в комментариях, очередь: считается заполненной, если putloc будет меньшим getloc либо если putloc1 находится в конце массива, a getloc — в начале массива. Таким образом, очередь будет пустой, если putloc и getloc являются эквивалентными. 5. И наконец, в файл lQDemo.cs поместите указанный ниже код класса DynQueue. При этом реализуется возрастающая очередь, размер которой увеличивается при необходимости. // Динамическая очередь, class DynQueue : ICharQ { char(] q; //В этом массиве хранится очередь int putloc, getloc; // the put and get indices // Конструирование пустой очереди заданного размера. public DynQueue(int size) { q ~ new char[size+1]; // Выделение памяти для очереди, putloc - getloc = 0; // Помещение символа в очередь, public void put(char ch) {
пользование интерфейсных ссылок 345 if(putloc==q.Length-1) { // Увеличение размера очереди. char[] t = new char[q.Length fc 2]; // Копирование for(int i=0; i t[i] = q[i]; элементов в новую очередь. < q.Length; i + + ) q - t; putloc+t; q[putloc] = ch; //' Выборка символа из очереди. public char get () { if(getloc == putloc) { Console.WriteLine(" - Очередь пуста."); return (char) 0; getloc++; return q[getloc]; При использовании этой реализации очереди в случае ее заполнения и попытки сохранения очередного элемента происходит создание нового базового массива, размер которого в два раза больше исходного. Содержимое текущей очереди копируется в массив, а затем ссылка на новый массив присваивается переменной q. 6. Для демонстрации возможностей трех реализаций интерфейса icharQ в файл lQDemo.cs введите код следующего класса. При этом для доступа ко всем трем очередям используется ссылка интерфейса icharQ. : / Демонстрация возможностей очередей. class IQDemo { public static void Main() { FixedQueue ql = new FixedQueue(10); DynQueue q2 ~ new DynQueue(5); CircularQueue q3 - new CircularQueue(10); ICharQ iQ; char ch; int i; iQ = ql; // Помещение нескольких символов в фиксированную очередь. for(i^0; i < 10; i++) iQ.put((char) ('A' + i)) ; (1 Отображение очереди. Console.Write("Содержимое фиксированной очереди: "); for(i=0; i < 10; i++) { ch = iQ.get () ; Console.Write (ch);
346 Глава 9. Интерфейсы, структуры и перечисти /' Помещение нексоорых символов в динамическую очередь. for(1-0; i < 10; 1 + -»-) xQ.put((char) ('Z’ - D); // Отображеи/ю очереди. Console.Write("Содержимое динамической очереди: ”); for(t-0; i •> 10; д + l ; ch - iQ.qet (); Console.Write (ch); } Console.WriteLine (); iQ - q3; // Помещение некоторых символов в круговую очередь. for(i-0; 1 < 10; 1т + ) IQ.put ( (char) СА’ т 1)); i I Отображение очереди. Console.Write("Содержимое круговой очереди: "); for (г-0; г < 10; 1 + -) { ch - iQ.get(); Console.Write(ch); 1 Console.WriteLine(); // Помещение дополнительных символов в круговую очередь. for(1-10; 1 < 20; 1н+) iQ.put( (char) ('А' т 1)); // Отображение очереди. Console.Write("Содержимое круговой очереди: "); for (1-0; 1 - 10; о-) { ch - iQ.getO; Console.Write(ch); ) Console.WriteLine("\пСохранение и использование" + " круговой очереди."); .* / Сохранение и использование круговой очереди. fcrd-C; 1 < 20; !-<-+) < iQ.put((char) ('А' - i)); ch - iQ.get () ; Console.Wrile(ch); 7. Скомпилируйте программу, задав включение в нее файлов icharQ. cs и IQDe' mo.cs. 1
двойства интерфейса 347 g Ниже приводится результат выполнения программы: Содержимое фиксированной очереди: ABCDEFGHIJ Содержимое динамической очереди: ZYXWVUTSRQ "одержимое круговой очереди: ABCDEFGHIJ Содержимое круговой очереди: KLMNOPQRST Сохранение и использование круговой очереди. ". SCDEFGHIJKLMNOIPQRST 9 \ сейчас будут продемонстрированы некоторые вещи, которые вы можете выпол- нять самостоятельно. В интерфейс I Спа г Q добавьте метод reset (), с помощью которого переустанавливается очередь. Создайте метод static, посредством которого копируется содержимое очереди одного типа в очередь другого типа. Двойства интерфейса Как и в случае с методами, при определении свойств в интерфейсе не указывается тело. Ниже приводится общая форма спецификации свойства: // 1-вой ство интерфейса суре name { } Конечно, get и set представляют свойства только для чтения и свойства для записи, соответственно. Ниже приводится продукт переработки интерфейса Series, а также код класса ByTw js, который использует свойство для получения и задания следующего элемента набора: // Использование свойства в интерфейсе. usin.j System; public interface Series { Свойство интерфейса _________________ “ next {4-----——— -------------——{Объявление свойства в интерфейсе Series. | get; // Возврат следующего числа в наборе. iset; // Установка следующего числа. ' Реализация интерфейса Series. ByTwos : Series { val; --•olic ByTwos () { val - 0; ' Получение или установка значения. DlJOlic int next { ◄-------------——Реализация свойства. qet I val +- 2; return val:
Глава 9. Интерфейсы, структуры и перечисти set { val - value; Н fl fl // Демонстрация свойства интерфейса. class SeriesDemo3 { public static void MainO { ByTwos ob = new ByTwos (); 11 Доступ к набору с помощью интерфейса. ,'^Н for (int i-0; i < 5; i++) Console.WriteLine("Следующее значение " + ob.next); Console.WriteLine("\пНачало c 21"); . jB ob.next - 21; JS for (int i-0; i < 5; 1++) Console.WriteLine("Следующее значение " + ob.next); 1 W } , Я Результат выполнения программы: ж Следующее значение 2 йЯ| Следующее значение 4 Следующее значение 6 “«Ж Следующее значение 8 Следующее значение 10 -ЗА Начало с 21 :Ж Следующее значение 23 Л Следующее значение 25 Ц' Следующее значение 27 fl Следующее значение 29 Ж Следующее значение 31 Ж Интерфейсные индексаторы 1 Обратите внимание на общую форму индексатора, объявленного в интерфейсе: // Индексатор интерфейса -f element-type this[int index] { get; Д set; } '? Как и ранее, лишь get и set представляют собой индексаторы, предназначенные только для чтения и только для записи, соответственно. Ниже приводится сше одна версия интерфейса Series; в ней добавляется индекса- тор, предназначенный только для чтения и возвращающий i-й элемент набора. t // Добавление индексатора в интерфейс, using System; public interface Series { // Свойство интерфейса
349 ИнтерФейсные индексаторы next { get-; // Возврат следующего числа в наборе. set; // Установка следующего числа. I !' Индексатор интерфейса. _____________________________________________- . \ „ tьis [int index] { ◄————[Указание индексатора, предназначенного только для чтения. get; // Возврат указанного числа в наборе. } (' реализация интерфейса Series. class ByTwos : Series { int val; public ByTwos() { val - 0; } // Чтение либо задание свойства, public int next { get { val +- 2; return val; set { val = value; / :/ Получение свойства с помощью индекса. public int this [int index] { <——| Реализация индексатopaT] get { val = 0; for(int i-0; i < index; i++) val 4-= 2; return val; } I' Демонстрация индексатора интерфейса. class SeriesDemo4 { public static void Main() { ByTwos ob = new ByTwos(); // Доступ к набору с помощью свойства for(int 1=0; i < 5; i++) Console.WriteLine("Следующее значение ” + ob.next); Console.WriteLine (”\пНачало c 21"); ob.next = 21; for(int 1=0; i < 5; 1++) Console-WriteLine ("Следующее значение ’’ + ob.next);
350 Глава 9. Интерфейсы, структуры и перечисле! Console . WriteLine ("\п11ереустаноька в 0"); ob.ne.xL 0; /{ Доступ к набору с помощью индексатора for (int -l-О; 1 < 1+») Console .WriteLine (’’Следующее значение " ос•ii); Результат выполнения программы: Следующее Следующее Следующее Следующее Следующее значение 2 значение 4 значение 8 значение 10 Начало с 21 Следующее значение 23 Следующее значение 2Ь Следующее значение 27 Следующее значение 29 Следующее значение 31 Переустановка в О Следующее значение О Следующее значение 2 Следующее значение 4 Следующее значение 6 Следующее значение 8 ® Минутный практикум I- Может ли переменная ссылки интерфейса указывать на объект, который 5’ \ ч) реализует этот интерфейс? , .. , 9 2. Может ли свойство интерфейса оыть предназначенным только для чтения.' 3. Имеют ли тела индексатор и свойства, определенные внутри интерфейса?* Наследование интерфейсов Возможно наследование интерфейсов. В этом случае используется синтаксис, весьма напоминающий синтаксис наследования классов. Если класс реализует интерфейс, который наследует другой интерфейс, должна обеспечиваться реализация для всех членов, определенных в составе цепи наследования интерфейсов. Обратите внимание на следующий пример: // Один, интерфейс может наследовать другой интерфейс, using System; public interface A { 1. Да. 2. Да. 3. Нет.
наследование интерфейсов 351 vo.d methlО; vo.Ld m.eth2(); . Интерфейс В включает методы methl () и meth2() ic interface В : A {-<-------jв наследует A, | r '?-d meth3(); он добавляет meth3(). htot класс должен реализовывать все методы А и В c:ass MyClass : В { • .«Lie void methl() { dunsole.WriteLine("Реализация methl(). ") ; .iblic void meth2() { Console.WriteLine ("Реализация rneth2() . iblic void meth3() { Console.WriteLine ("Реализация rneth3() cl'iss IFExtend { rublic static void Main() { MyClass ob - new MyClass(); ob.methl(); ob.metb2(); ob.meth3(); - - ....- ... .. ..—..... — Вопрос. Если один интерфейс наследует другой интерфейс, можно ли объя- вить член наследующего интерфейса, скрывающего член, определенный базовым интерфейсом? Ответ. Да. Если сигнатура члена наследующего интерфейса совпадает с сигнатурой одного из наследуемых интерфейсов, имя наследуемого интерфейса будет сгрыто. Как и в случае с наследованием класса, подобное сокрытие приведет к соображению предупреждающего сообщения независимо от того, будет ли указан 'ен наследующего интерфейса с помощью ключевого слова new. В качестве эксперимента, можно попытаться удалить реализацию метода methl О в и iacce MyClass. Это приведет к появлению ошибки компиляции. Как упоминалось Гансе, любой класс, реализующий интерфейс, должен реализовывать все методы, которые определены с помощью данного интерфейса, включая те из них, которые наследуются из других интерфейсов.
352 Глава 9. Интерфейсы, структуры и перечислв! Явная реализация При реализации члена интерфейса можно полностью квалифицировать его имя.* качестве имени интерфейса. В результате создается явная реализация члена интерф^ са, либо, сокращенно, явная реализация. Например, следующий код Interface IMylF { int myMeth(int x) ; } допустим при реализации iMyiF, как показано ниже: class MyClass : ImylF { int ImylF.myMeth(int x) return x / 3; Полностью определенное имя используется при создании явной реализации. Как видите, при реализации члена myMeth О класса IMyiF указывается его полней имя, включая имя его интерфейса. Необходимость явной реализации для члена интерфейса может обосновываться двуц| причинами. Во-первых, класс может реализовывать два интерфейса, причем вобсф объявляются методы с помощью одного и того же имени и типа сигнатуры. Благодаря полному определению имен устраняется двусмысленность подобной ситуации. Во- вторых, при реализации метода посредством его полностью определенного имени определяется объем частной реализации, который не доступен для кода, не относя- щегося к классу. А сейчас перейдем к рассмотрению практических примеров. Следующая программа содержит интерфейс lEven, в котором определяются два метода: isEven О и isOdd(). Эти методы используются для определения того, яв- ляется число четным либо нечетным. Затем класс MyClass реализует интерфейс lEven. После этого явно реализуется метод isOdd (). // Явная реализация члена интерфейса. using System; interface lEven ( bool isodd(int x) ; bool isEven(int x) ; class MyClass : lEven { // Явная реализация. bool IEven.isOdd(int x) {—- if((x%2) !- 0) return true; else return false; } Явно реализован метод isOdd (). В результате он будет действительно частным. // Обычная реализация. public bool isEven(int x) { lEven о =- this; // Ссылка на вызывающий объект. } return !о.isOdd(х); }
^цая реализации 353 ,ia<s Demo { ’public static void Main() { MyClass ob = new MyClass (); oool result; result = ob. is Even (4 ) ; if(result) Console.WriteLine("4 четно."); else Console.WriteLine("3 нечетно."); :/ result = ob.isOdd(); // Ошибка, не открыто. Поскольку метод Oddo реализован явно, он недоступен вне класса MyClass. Благо- даря этому реализация станет по-настояшему эффективной. Метод isOddt) внутри класса MyClass может стать доступным только после того, как на него будет сделана ссылка из интерфейса. Причина этого заключается в том, что он вызывается с помощью реализации о в методе isEven (). Ниже приводится пример, в котором два интерфейса реализуются и объявляются с помощью метода meth(). С целью преодоления неоднозначности, возникающей в подобной ситуации, используется явная реализация. /7 Использование явной реализации с целью устранения неоднозначности, gsir.g Sys rem; interface IMyIF_A { int meth(int x); <- ы для двух этих методов совпадают. interface IMylFJB { int meth (int x) ; <- i В классе MyClass реализуются оС>а интерфейса. e-ass MyClass : IMyIF_A, IMyIF_B ( iMy!F_A a_ob; i>-ylF_B b__ob; Явная реализация двух методов meth(). t п у I My I F_A. me t h (i n t x) { ◄ .—-—| При явной реализации устраняется неоднозначность, return х + х; I i£-t IMyIF_B.meth (int x) {◄---------------------------------' return x * x; Вызов метода meth() с помощью ссылки интерфейса. Public int methA(int x){ a_ob = this; return a_ob.meth(x); // Вызов IMyIF_A Public int methB(int x){ b_ob = this; return b_ob.meth(x); // Вызов IMylF В
354 Глава 9. Интерфейсы, структуры и перечисли class FQIFNames { .М public static void MainO { MyClass ob --- new MyClass (); Console.Write("Вызов метода IMylF A.meth(): W Console.WriteLine(ob.methA(3)); ‘1Я Console.Write("Вызов метода IMylF _B.meth () : ”); Console.WriteLine(ob.methB(3)) ; } Я Результат выполнения программы: Щ Вызов метода IMyIF_A.meth(): 6 Я Вызов метода IMylF В.meth () : 9 ./«К При просмотре кода программы обратите внимание на то, что метод meth () иммК одни и те же сигнатуры в IMylF А и в IMylF в. Таким образом, в случае, если MyClass реализует оба этих интерфейса, он должен явно реализовать каждый из л по отдельности, полностью квалифицируя их имена в ходе осуществления процефД Поскольку единственным способом, посредством которого может быть вызван явз® реализованный метод, является ссылка интерфейса, метод MyClass создает д® подобных ссылки, одна из которых предназначена для IMylF_а, а вторая — iMyiF B. Затем вызываются оба собственных метода класса с помощью метод» интерфейса, в результате чего устраняется неоднозначность. Ж. Я © Минутный практику/л t 1'% 1- Если интерфейс А наследуется интерфейсом В, а интерфейс В наследуете^ \ чи классом С, то может ли класс С реализовывать вес члены классов А и В' либо только те из них, которые определены классом В? ф 2. Назовите две причины, в силу которых может потребоваться явная реалий ация интерфейса. ? Структуры Как вы знаете, классы представляют собой ссылочные типы. Это означает, что достуй к объектам классов производится с помощью ссылок. Это в корне отличается оттиПОЯ значений, доступ к которым осуществляется непосредственно. Однако иногда бывав® полезно обеспечить непосредственный доступ к объекту в силу соображений эффеК' тивности. Доступ к объектам класса с помощью ссылок влечет дополнительньй! накладные расходы при каждом сеансе доступа. При этом также потребляется дополнительное пространство на жестком диске. При работе с очень маленькиМЯ объектами величина дополнительного потребляемого пространства может иметь большое значение. С целью решения подобных проблем в C# предлагаются струй’ туры. Структура подобна классу, но она имеет скорее тип значения, но никак не тИ]® ссылки. у 1. Реализующий класс должен включать все члены, указанные в иерархии интерфейсов. " 2. Явная реализация может использоваться для устранения неоднозначности, а также Для препятствия отображению члена интерфейса. а
Структуры 3 объявление структур выполняется с помощью ключевого слова struct. Так асаует отметить, что структуры синтаксически подобны классам. Ниже приводи' обобщенная форма объекта struct: ,Г!К-С name : interfaces { struv.. - // Объявления членов 0мя структуры указывается с помощью параметра name. Структуры не могут наследовать другие структуры либо классы, а также нс мог применяться в качестве основы для других структур либо классов. Однако кажд структура может реализовывать один иди больше интерфейсов. Реализуемые интс фейсы указываются после имени структуры в виде списка, разделенного запятым! Как и в случае с классами, члены структуры включают методы, поля, индексатор свойства, операторные методы, а также события. Кроме того, структуры мог определять конструкторы, но не деструкторы. Однако для структуры не может бы определен конструктор, заданный ло умолчанию (без параметров). Причина это] заключается в том, что конструктор, заданный по умолчанию, автоматически опр« делястся для всех структур и не может быть изменен. Объект структуры может быть создан с помощью ключевого слова new точно так ж» как объект класса (но не обязательно). Как только будет использовано ключевое слое new, вызывается указанный конструктор. Если названное ключевое слово не npv меняется, объект по-прежнему создастся, но не инициализируется. Благодаря этом не требуется выполнять инициализацию вручную. Ниже приводится пример использования структуры: // Демонстрация возможностей структуры, using System; // Определение структуры. _____________ struct account { <———----------—•—|Определение структуры. puolic string name; public double balance; Public account(string n, double b) { name - Dr- balance -- b; Z/ демонстрация возможностей структуры account. cJ-uss StructDemo { public static void Main() { account accl - new account("Том", 1232.22); // Явный конструктор. account acc2 - new account О; // Конструктор, заданный по умолчанию. account ассЗ; 11 Конструктор отсутствует. Console .WriteLine (accl. name + " имеет на счету " -* accl.balance) ; Console.WriteLine() ; if(acc2.name - null) Console.WriteLine("acc2.name равно null."); Console.WriteLine("acc2.balance равен ’* + acc2.balance); Console.WriteLine();
356 Глава 9. Интерфейсы, структуры и перечи // Должен инициализировать ассЗ до его использования. ассЗ.пате - "Мэри"; ассЗ.balance = 99.33; Console.WriteLine(ассЗ.name + ” имеет баланс ассЗ.balance); Результат выполнения программы: Том имеет на счету 1232.22 асс2.пате равно null. асс2.balance равен О J Мэри имеет на счету 99.33 | Ответы профессионала - -— .—— Вопрос. Известно, что в C++ также имеются структуры и используя ключевое слово struct. Подобны ли структуры языков программирования Сд C++? 1 Ответ. Нет. В С-г+ ключевое слово struct определяет тип класса. Та! образом, в C++ ключевые слова struct и class практически эквивалентны. (Разни заключается в способе реализации доступа к членам класса, заданного по умой нию.) В C# struct определяет тип значения, a class определяет тип ссылки, | В примере программы структура инициализируется либо посредством ключей слова new, используемого для вызова конструктора, либо посредством прося объявления объекта. Если используется ключевое слово new, выполняется инии лизация полей структуры. Процесс инициализации происходит либо с помои конструктора, заданного по умолчанию, который инициализирует все поля с щыо стандартных значений, либо путем использования конструктора, определен!! пользователем. Если ключевое слово new нс применяется, объект не инициализируй а его полям должны быть назначены значения до использования объекта. Перечисления j Под перечислением понимается набор именованных целочисленных констант, о* дсляюших вес допустимые значения переменной для данного типа. Подобный?, объектов весьма распространен. Ниже приводится перечисление, включающее О сание монет США: penny, nickel, dime, quarter, half-dollar, dollar J Ключевое слово enum определяет перечислимый тип. Рассмотрим общую фо! используемую при определении перечисления: i enum name { enumeration list }; Имя типа, используемого в перечислении, указывается с помощью параметра Л Список enumeration list представляет собой список идентификаторов, разД® пых запятыми.
числения рс1сГ*- юшсм ФРагментс кода определяется перечисление coin: coin { penny, nickel, dime, quarter, half-dollar, dollar}; enUd „ ^hUm u понятии «перечисление» является тот факт, что каждому из симво В ответствует целочисленное значение. В результате область применения симво С впадаст с областью использования целочисленных значений. Целочисленное з чение для каждого символа больше значения предшествующего символа. По ум чанию значение первого символа перечисления равно нулю. доступ к элементам перечисления осуществляется посредством имен типов, с пользованием точечного оператора. Обратите внимание на код console .ATxteLine ("penny is " + coin, penny < который отображает следующее сообщен нс: penny is 0 nickel is 1 Ниже приводится пример программы, демонстрирующей возможности перечисле! coin: // Демонстрация возможностей перечисления using System; class EnuirDemo { enum coin t penny, nickel, dime, quarter, half_dollar, dollar }; public static void Main() { string[J names = { ’’penny", "nickel", "dime", "quarter", "half_dollar", "dollar" corn i; // Объявление переменной для обработки перечисления // Использование переменной i для цикла ____________ // обработки перечисления Переменная coin мож( for(! _ coin.penny; i <= coin.dollar; i + + )-<---------[контролировать цикл f Console.WriteLine(names[(int)i] t " имеет значение " + i); езУльтат выполнения программы: Pe»riy , ^-Ч'-ет значение О ‘“':еет значение 1 значение 2 hai₽ter ИК1«ет значение 3 dev- w--^ar имеет значение 4 имеет значение 5 ^ерТТИГС вниманис на то- что управление циклом for осуществляется с помош 1 еМсн"ой, имеющий тип coin. Поскольку перечисление имеет целочисленн
358 Глава 9. Интерфейсы, структуры и перечи. тип, значение перечисления может присваиваться там же, где могут назначат целочисленные значения. Однако при использовании значения псречислени, целью индексации массива names требуется провести преобразование типа. С дпу стороны, как только значения перечислимых значений в coin начинаются с ц\ эти значения могут использоваться при индексировании массива names с це; получения названий монет. Техника, заключающаяся в сопоставлении осмыслен! строк и значений перечисления, является весьма полезной. Инициализация перечисления й С помощью инициализатора можно указать значения одного или большего колиц^ ства символов. Для этого после каждого символа должен следовать знак равенствах целочисленное значение. Символы, отображаемые после инициализаторов, предстай. ляют собой назначенные значения, которые должны быть больше, чем предыдущ^ значение инициализации. Например, в следующем коде значение 100 присваивается символу quarter: ,5 enuir coin { penny, nickel, dime, quarter-100, half_dollar, dollar!; Теперь символы имеют такие значения: penny 0 nickel 1 dime 2 quarter 100 half_dollar 101 dollar 102 Указание базового типа перечисления По умолчанию перечисления основываются на типе int, но можно создавать перечисления любого целочисленного типа, за исключением char. Для указания типа, отличного от int, поместите наименование базового типа после имени пере- числения, разделив их между собой двоеточием. Например, после применения следующего оператора coin становится перечислением, основанным на типе byte, enum coin : byte i penny, nickel, dime, quarter, half-dollar, dollar}; Теперь, например, значение, присваиваемое члену coin.penny, имеет тип byte. Минутный практикум 1. Нужно ли при создании структуры пользоваться ключевым словом new? ' J 2, Может ли для структуры определяться деструктор? 3. Что такое перечисление? 1. Нет, объект struct может создаваться подобно объекту любого другого типа без использо- вания ключевого слова new. Однако при этом объект не будет инициализирован. 2. Нет, структуры не могут обладать деструкторами. 3. Перечисление представляет собой перечень именованных целочисленных констант.
доНтР0ЛьНые вопросы Контрольные вопросы 1. Ключевой афоризм для C# звучит гак: «Один интерфейс, много мето- дов». Какие свойства лучше всего иллюстрируют эго высказывание? 2. Сколько классов может реализовать интерфейс? Сколько интерфейсов может реализовать класс? 3. Могут ли наследоваться интерфейсы? 4. Должен ли класс реализовывать все члены интерфейса? 5. Может ли интерфейс объявлять конструктор? 6. Создайте интерфейс для класса Vcnicle (глава 7). Назначьте ему имя IVenicle. 7. Создайте интерфейс для безопасных массивов. Добейтесь этого путем адаптации массива, устойчивого к отказам, из последнего примера главы 7. 8. Каковы различия между struct и class? 9. Покажите, каким образом можно создать перечисление для планет солнечной системы. Назовите его arrets.
g4<w(KW Обработка ~ исключений □ Использование блоков try и catch □ Если исключение не перехвачено... □ Использование нескольких операторов catch □ Вложенные блоки try □ Генерирование исключений □ Объект Exception □ Использование ключевого слова finally □ Исследование встроенных исключений C# □ Создание собственных классов исклю- чений □ Использование ключевых слов checked и unchecked
System.Exception 361 авС рассматривается обработка исключений. Исключение представляет собой <6kv. происходящую во время выполнения программы. С помощью подсистемы л яботки исключений для C# можно обрабатывать такие ошибки, не вызывая краха 0 граммы. Решение этой задачи в C# основано на усовершенствованных методиках, вменяемых в языках программирования C++ и Java. Поэтому для читателей, 'дакомых с основами одного из этих языков, эти методики уже известны. Однако обработка исключений в C# отличается четкостью и простотой реализации. р|алич,,е механизма обработки исключений позволяет упростить процесс написания кола Ранее код обработки исключений полностью создавался программистом. Па- пример, если язык программирования не имеет средств обработки исключений, то необходимо предусмотреть возврат методами кодов ошибок, возникающих в резуль- тате сбоев программы, и обработку этих кодов. Подобный подход неудобен и чреват возникновением новых ошибок. Обработка исключений ускоряет процесс обработки ошибок, поскольку программа может содержать блок кода, именуемый обработчиком исключении. Этот блок кода будет выполняться автоматически в случае возникновения каких-либо ошибок. Необязательно вручную проверять правильность выполнения каждой отдельной операции или вызова метода. При появлении ошибки происходит ее обработка с помощью обработчика ошибок. Другая причина, по которой важна обработка исключений, состоит в том, что в C# определены стандартные исключения для наиболее часто встречающихся ошибок, таких как деление на ноль или выход значения индекса за пределы диапазона. Поэтому для устранения подобных ошибок в программе нужно только следить за возникновением соответствующих исключений и обрабатывать их. Таким образом, для успешного овладения языком программирования C# необходимо всовершенстве освоить методику работы с подсистемой С#, выполняющей обработку исключений. hacc System.Exception в языке С" исключения представлены классами. Все классы исключений могут быть Унаследованы от встроенного класса исключений Exception, являющегося частью Пространства имен System. Поэтому все исключения представляют собой подклассы “пасса Exception. Из к класса Exception наследуются классы исключений Systerr.Exception и Appli- nSy-oeption. Таким образом, в C# определяются две общие категории исклю- и. образованные средой исполнения C# (то есть CLR) и сгенерированные н Очными приложениями. Ни SystemException, ни ApplicaticnExcepticn ^^яют ничсго нового в класс Exception. Эти классы фактически определяют 11с ючки двух различных иерархий исключений. В t6rng °пРсДслены несколько встроенных исключений, наследуемых из класса Sys- Искл ‘ ''ent lori- Например, если возникает ситуация с делением на ноль, генерируется Осц0^ЧСНие ^ivideByZeroException. Как будет отмечено далее в этой главе, на “ЗТь Kj,acca исключений ApplicationException пользователь может сформиро- . Св°и собственные классы исключений.
362 Глава 10. Обработка исключи Блок, кода, для которого выполняется мониторинг ошибок. (ЕхсерТуре 1 ехОЬ) { Обработчик исключений ExcepTypel. л (ЕхсерТуре2 ехОЬ} { Обработчик исключений ЕхсерТуреЕ. Основы обработки исключений Обработка исключений в C# выполняется с применением четырех ключевых сло^В try, catch, throw и finally. Эти ключевые слова образуют взаимосвязанную® подсистему, в которой использование одного из ключевых слов влечет за соблЙЯг использование других. Все упомянутые ключевые слова будут подробнее рассмотрены» ниже. Однако сначала полезно получить общее представление о той роли, которая® отведена при обработке исключений каждому из этих ключевых слов. Рассмотрим®, вкратце функции каждого из них. Л В блок try помещаются операторы программы, за выполнением которых необходимо® следить на предмет возникновения исключений. Возникающее исключение перехва.® тывается и обрабатывается блоком catch. Для генерирования исключения применял, ется ключевое слово throw. В блок finally помещается код, который всегда® выполняется при выходе из блока try. Д Использование блоков try и catch | Обработка исключений основана на использовании блоков try и catch. Эти блоки,'| работают совместно: нельзя задать блок try и не задать catch, и наоборот. Ниже Д' приводится общий синтаксис блоков try/catch, выполняющих обработку исклю-ф чсний: | try { // } catch // } catch /7 } Параметры ЕхсерТуре задают типы обрабатываемых исключений. После генерирования/ исключения происходит его перехват соответствующим оператором catch, выпол-Д няющим обработку этого исключения. Как показано в общей форме синтаксиса, С' блоком try может ассоциироваться более одного оператора catch. Тип исключения;, определяет выполняемый оператор catch. Если возникшее исключение соответствует типу исключения, указанному в операторе catch, то выполняется блок команд этого оператора (а все остальные блоки catch игнорируются). При перехвате исключения *! оператором catch соответствующий объект исключения ехОЬ получает его значение. | Фактически указание объекта ехОЬ не является обязательным. Если обработчик исключений не нуждается в доступе к объекту исключения (это часто бывает на практике), нет необходимости в указании ехОЬ. Учитывая сказанное, для многих примеров в главе объекты исключения задавать нс надо. Важно отметить, что если исключение не генерируется, работа операторов блока try завершается в обычном порядке, а все соответствующие ему операторы catch,» пропускаются. Выполнение программы возобновляется с первого оператора, следую- I шего за последним оператором catch. Таким образом, операторы catch вызываются | только в том случае, если генерируется исключение. Ж
основы обработки исключений 363 Простои пример исключения ^иже приводится простой пример, демонстрирующий методику отслеживания и перехвата исключений. Вы, наверное, знаете, что при обращении к индексу, выхо- дящему за пределы массива, возникает ошибка. В этом случае система выполнения (2# генерирует исключение IndexOutOfRangeException, представляющее собой стандартное исключение. определенное в С#. Назначение следующей! программы заключается в генерировании и перехвате подобного исключения: I. демонстрация обработки исключений, using System; class ExcDemol { public static void Main() { iritf] nums - new int(4]; Console.WriteLine("Эта строка выводится до генерирования исключения." Попытка использования индекса массива nums, находящегося за преде- лами диапазона. // Генерирование исключения вследствие выхода индекса за пределы диапазона, nums [7} 10; <-—— --— — -------------------—— Console.WriteLine("Этот текст не будет отображен."); } catch (IndexOutOfRangeException) { // Перехват исключения. Console.WriteLine("Индекс за пределами диапазона!"); } Console-WriteLine ("После оператора catch."); Результат выполнения программы: Эта строка выводится до генерирования исключения. Индекс за пределами диапазона! После оператора catch. В данной программе кратко рассматриваются некоторые основные моменты, связан- ные с обработкой исключений. Во-первых, код, который требуется отслеживать на предмет выявления ошибок, заключается в блок try. Во-вторых, в случае возникно- вения исключения (в данном случае — по причине попытки использования значения индекса массива nums. выходящего за пределы диапазона) таковое перехватывается оператором catch. Начиная с этого момента контроль передается блоку catch, а блок try завершает свою работу. При этом блок catch фактически не вызывается. Скорее, этому блоку передается управление. В результате оператор VJritehine (), оледующий за оператором использования индекса, значение которого выходил за 'ределы диапазона, выполнен нс будет. После выполнения оператора catch управ- ление передается операторам, следующим за блоком catch. Решение проблем, 1!|>1зывающих возникновение исключения, — удел обработчика исключений. При j'om программа продолжает выполняться и нс завершается аварийно. Обратите внимание на то, что в конструкции catch нс указан ни один параметр. Как упоминалось ранее, параметр требуется только тогда, когда необходим доступ к объектам исключений. В некоторых случаях значение объекта исключения может быть ^пользовано обработчиком исключений с целью получения дополнительных сведений L
364 Глава 10. Обработка исключи об ошибке, но часто достаточно самого факта возникновения исключения. Гакщ образом, отсутствие параметра catch в обработчике исключений (как в предыдущ^ примере программы) нс является чем-либо необычным. Ранее уже упоминалось о том, что если в блоке try исключение не возникав операторы catch нс вызываются и управление программой передается оператору следующему после оператора catch. С целью проверки Этого факта в лредыдущ^ примере программы замените строку nums [7] = 10; строкой n ums [ С ] =- 10; В результате исключение нс возникнет, а блок catch нс будет вызван. Второй пример исключения Важно понимать, что весь код, входящий в состав блока try, контролируется и предмет наличия исключений. При этом указываются исключения, которые могу генерироваться методом, вызываемым внутри блока try. Исключения подобной типа могут перехватываться этим же блоком try (предполагается, что метод сам щ себе не выполняет перехват исключений). Ниже приводится пример корректно! программы: //* Исключение может 1’енерироваться с помощью одного метода, а затем перехватываться другим методом. */ using System; class ExcTest { // Генерирование исключения. public static void genException() { int [ j nums •= new int[4]; Console.WriteLine("Эта строка выводится до генерирования исключения.; // Генерирование исключения в случае выхода индекса за пределы диапазона nums[7] => 10; < -- -----——-—-—------—---------——[^есьгенерируетсяис>О1ЮчёниеГ| Console - WriteLine (“Этот текст не будет отображен.’’); class ExcDemo2 { public static void MainO { try { ExcTest.genException() ; } catch (IndexOutOfRangeException) [ -------[Здесь перехватывается исключениС- // catch the exception Console.WriteLine(“Индекс за пределами диапазона!"); } Console.WriteLine("После оператора catch.");
£СЛи исключение не перехвачено 365 результат выполнения этой программы аналогичен результату выполнения предыдущей: 3 строка выводится до генерирования исключения. индекс за пределами диапазона! После оператора catch. ]4ак только метод genException () вызывается внутри блока try, генерируемое в результате исключение (если оно до сих пор не перехвачено) перехватывается с помощью оператора catch, входящего в состав метода Main(). Понятно, что если метод genException () перехватит исключение, оно уже не будет возвращено методу Ка г п () • Минутный практикум I. Что такое исключение? 2. Частью какого оператора должен быть код, отслеживаемый исключениями? 3. Каковы функции оператора са tch? Что происходит после выполнения этого оператора? Если исключение не перехвачено Перехват и обработка одного из стандартных исключений C# (выполняемые в предыдущем примере) предотвращают аварийное завершение программы. После возникновения исключения оно должно быть перехвачено и обработано неким фрагментом кода. Если программа не перехватывает исключение, последнее будет перехвачено системой выполнения С#. Проблема, появляющаяся в этом случае, заключается в том, что система выполнения отображает сообщение об ошибке и завершает выполнение программы. Например, в следующей версии предыдущего примера исключение, возникающее в результате выхода за пределы диапазона, не перехватывается программой: // Позволяет системе выполнения C# обрабатывать ошибки, using System; class NotHandled { public static void MainO { int[] nums - new int[4]; Console.WriteLine("Эта строка выводится до генерирования исключения."); // Генерирование исключения при выходе значения индекса за пределы // диапазона. nums [ 7] = 10; 1 В роли исключения выступает ошибка времени выполнения. 2. Для отслеживания исключений потребуется включить код в состав блока try. 3. Оператор catch перехватывает исключения. Так как оператор catch не вызывается из программы, то после выполнения блока catch управление не передается обратно оператору программы, при выполнении которого возникло исключение. Выполнение программы продолжается с операторов, находящихся после блока catch.
366 Глава 10. Обработка исключен! В этом случае выполнение программы прерывается и отображается следующие сообщение об ошибке: Д Unhandled Exception: System.IndexOutOfRangeException: ;Л Exception of type System.IndexOutOfRangeException 4 was thrown. J| at NotHandled.Main () Хотя сообщения подобного тина являются весьма полезными в случае отладки]! весьма нежелательно, чтобы их видели пользователи данной программы! По этом причине важно, чтобы программа сама обрабатывала исключения. Ранее уже упоминалось о том, что тип исключения должен соответствовать типу указанному с помощью оператора catch. Если это не так, исключение может бытш не перехвачено. Например, в следующей программе предпринимается попытки обработки ошибки, возникающей! вследствие выхода за границы массива, путем! перехвата оператором catch исключения DivideByZeroException (еше одно встроим енное исключение С#). Как только будут перейдены границы массива, генерируется исключение indexOutOfRange, которое не перехватываегея оператором саТсад В результате происходит аварийное завершение программы. ,1 // Эта программа неверно обрабатывает исключение! Ц using System; Й class ExcTypeMismaoch { public static void Main() { •Q int[] nums = new int[4]; Й try 1 1 Console .WriteLine ("Эта строка выводится до генерирования исключения.’’); >3 // Генерирование исключения выхода значения индекса массива за пределы $ / / диа п а з о на. _____________________________________________” nums [ 7 ] = 10; *4 ---—|Здесь генерируется исключение indexoutOfKangeException.|’/ Console.WriteLine("Этот текст не отобразится."); } 1 /* Невозможно перехватить ошибку выхода за границы массива .! с помощью исключения DivideByZeroException. р/., catch (DivideByZeroException) { <........ Попытка перехвата ошибки с помощью // Перехват исключения исключения DivideByZeroException^ Console.WriteLine("Индекс за пределами диапазона’"); } Console.WriteLine("После оператора catch."); Результат выполнения программы: Эта строка выводится до генерирования исключения. Unhandled Exception: System.IndexOutOfRangeException: ; Exception of type System.IndexOutOfRangeException was thrown. : at ExcTypeMismatch-Main() !
рзящная обработка ошибок с помощью исключений 367 результаты выполнения программы показывают, что оператор catch, рассчитанный а исключение DivideByZeroException, не перехватывает исключение Index- prPanqeException. 03ЯЩНЭЯ обработка ошибок с помощью исключении Одним 113 основных преимуществ обработки исключений является то, что в этом случае программа может реагировать на ошибки, а затем продолжать выполнение. Обратите внимание на следующий пример, в котором элементы одного массива делятся на элементы другого. Если произошло деление на ноль, генерируется исключение DivideByZeroException. В программе выполняется обработка исклю- чения с отображением сообщения об ошибке, а затем программа продолжает выпол- няться. Таким образом, ситуация деления на ноль не приводит к появлению ошибки выполнения с последующим аварийным завершением работы программы. Вместо этого происходит изящная обработка ошибки, в результате которой возможно про- должение выполнения программы. // Изящная обработка ошибок и продолжение выполнения программы, using System; class ExcDemo3 { puolic static void Main() { rnt[] numer = { 4, 8, 16, 32, 64, 128 j; int[] denom - ( 2, 0, 4, 4, 0, 8 }; for(int i-0; i<numer.Length; i++) { try { Console.WriteLine(numer[i] *-”/" + denom[i] + " будет " + numer[i]/denom[i]); I catch (DivideByZeroException) { // Перехват исключения Console-WriteLine("Нельзя делить на ноль!”); Вог результат выполнения программы: / 2 будет 2 Нельзя делить на ноль! 4 будет 4 4 будет 8 Ющзя делить на ноль! / 8 будет 16 ° Ют пример демонстрирует еще один важный факт: как только исключение obpaoa- тЬгвается, оно устраняется из системы. Таким образом, в программе, в которой! блок t;y заключен в тело цикла, будут обрабатываться все исключения. Благодаря этому возможна обработка повторяющихся ошибок.
368 Глава 10. Обработка исключен Минутный практикум I. Каким должен быть тип исключения в операторе catch? 2. Что произойдет в случае, если исключение не будет перехвачено? 3. Что должна выполнять программа при возникновении исключения? Использование нескольких операторов catch С одним блоком try может быть связано более одного блока catch. Подобная практика является общепринятой. Однако каждый блок catch должен перехватывай- исключения различного типа. Приведенная ниже программа перехватывает ошибки' связанные с выходом индекса за границы массива и с делением на ноль. // Использование нескольких операторов catch, using System; class ExcDemo4 { public static void Main() { // Массив numer содержит больше элементов, чем denom. int[] numer - { 4, 8, 16, 32, 64, 128, 256, 512 }; int[] denom = { 2, 0, 4, 4, 0, 8 }; for (int i^O; iCnumer.Length; i++) { try { Console .WriteLine (numer [ i] + '*/” + denom [ i ] +• ” будет " + numer[i]/denom[i]); } catch (DivideByZeroException) { ч-------------------- // Перехват исключения r- h Console.WriteLine("Нельзя делить на ноль!"); с } L catch (IndexOutOfRangeException) (------------------- // Перехват исключения Console.WriceLine("Не найдены совпадающие элементы.") } Результат выполнения программы: 4/2 будет 2 Нельзя делить на ноль! 16/4 будет 4 32/4 будет 8 Нельзя делить на ноль! 1. Тип исключения в операторе catch должен соответствовать типу перехватываемого исклю- чения. 2. Неперехваченное исключение непременно приводит к досрочному прекращению выполне- ния программы. 3. Программа должна осуществлять изящную и рациональную обработку исключений. При этом нельзя оставлять необработанные исключения и следует избегать нестандартных завершений- операторов
Перехват всех исключений 369 ;28 / 8 будет 16 не найдены совпадающие элементы. Не найдены совпадающие элементы. Из результата выполнения программы видно, что каждый оператор catch соответ- ствует своему собственному типу исключения. Как правило, выражения catch проверяются в порядке их записи в программе. В итоге выполняется только тот блок, исключение которого совпадает с возникшим. Остальные блоки catch будут в этом случае проигнорированы. Перехват всех исключений Иногда требуется перехватывать все исключения, не обращая внимания на их тип. Для выполнения этой задачи воспользуйтесь оператором catch, для которого нс указываются параметры. Такой оператор перехватывает все исключения. В приведен- ном ниже примере возникающие исключения TndexOutOfRangeException и Di- videByZeroException перехватываются и обрабатываются одним и тем же блоком catch: / Использование .оператора catch, "перехватывающего все”, using System; class ExcDemo5 { public static void Main() { // Массив numer содержит больше элементов, чем denom. int[] numer - { 4, 8, 16, 32, 64, 128, 256, 512 }; inti] denom = { 2, 0, 4, 4, 0, 8 }; for(int i=0; i<numer.Length; i++) { try { Console. WriteLine (numer [i] -r ” / " + denom[i] + ” будет ” + numer[i]/denom[i] ) ; } -------------------------------------------------- catch {4_______________I Перехватываются все исключения. | Console.WriteLine("Возникло исключение."); ) } } Результат выполнения программы: 1/2 будет 2 Возникло исключение. 16/4 будет 4 32/4 будет 8 Возникло исключение. 128 / 8 будет 16 Возникло исключение. Возникло исключение.
370 Глааа 10. Обработка исключений J Вложенные блоки try. Вложенные блоки try Блоки try могут вкладываться друг в друга. Исключение, генерируемое во внутрен- нем блоке try и не перехваченное блоком catch, соответствующим данному внут- реннему блоку try, распространяется на внешний блок try. Ниже показан пример возникновения исключения IndexOutOfRangeException, которое не перехватыва- ется внутренним блоком try, но зато перехватывается внешним блоком try: // Использование вложенного блока try. using System; class NestTrys { public static void MainO { // Массив numer содержит больше элементов, чем denom. inc[] numer = { 4, 8, 16, 32, 64, 128, 256, 512 }; int[J denom = { 2, 0, 4, 4, 0, 8 }; try { // Внешний блок try^---------------- for(int i^O; i<numer.Length; i++) { try { // Вложенный блок try ....... — Console.WriteLine(numer[i] + ’’/” + denom[i] + " будет " + numer[i]/denom[i]) ; } catch (DivideByZeroException) { // Перехват исключения. Console.WriteLine("Нельзя делить на ноль’”); } } } catch (IndexOutOfRangeException) { // Перехват исключения. Console.WriteLine("Не найдены совпадающие элементы."); Console.WriteLine("Критическая ошибка - выполнение программы прервано."); } } } Результат выполнения программы: 4/2 будет 2 Нельзя делить на ноль! 16 / 4 будет 4 32 / 4 будет 8 Нельзя делить на ноль! 128 / 8 будет 16 Не найдены совпадающие элементы. Критическая ошибка - выполнение программы прервано. В этом примере исключение, возникающее при попытке деления на ноль, обрабаты- вается внутренним блоком try. При этом обеспечивается продолжение выполнения программы. Ошибка, связанная с выходом за границы массива, перехватывается внешним блоком try, который завершает выполнение программы. Хотя, конечно, использование вложенных операторов try обуславливается не един- ственной причиной, предыдущая программа создает важный прецедент, который
Генерирование исключений 371 может быть обобщен. Часто вложенные блоки try используются для обработки ошибок разных видов различными способами. Некоторые ошибки являются фаталь- ными и нс могут быть исправлены. Другие же являются нс столь значительными и могут быть обработаны немедленно. Многие программисты используют внешний блок try с целью перехвата большинства серьезных ошибок, позволяя внутреннему блоку try обрабатывать менее серьезные. Можно также воспользоваться внешним блоком try с оператором catch без параметров для перехвата тех ошибок, которые не обрабатываются внутренним блоком. Минутный практикум I. Каким образом могут перехватываться все исключения? 2. Может ли один блок try использоваться для перехвата двух или большего количества исключений различных типов? 3. Что происходит в случае, если исключение не перехватывается внутренним блоком при использовании вложенных блоков try? Генерирование исключений В предыдущих примерах производился перехват исключений, автоматически сгене- рированных С#. Однако исключение может быть сгенерировано и посредством оператора throw. Ниже показан синтаксис этого оператора: throw exceptOb; Здесь exceptOb является объектом класса исключений, наследуемым из класса .xcepticn. Далее приводится пример, показывающий, каким образом оператор throw обеспе- чивает генерирование исключения DivideByZeroException: '! Генерирование исключения оператором throw, .sing System; ’lass ThrowDemo { public static void Main() { try { Console.WriteLine("Перед использованием оператора throw new DivideByZeroException ();-^—--•------ } catch (DivideByZeroException) { // Перехват исключения. Console.WriteLine("Исключение перехвачено."); } Console.WriteLine("После блока try/catch; } throw. ); Генерирование исключения. I. Для перехвата всех исключений воспользуйтесь оператором catch без указания параметра исключения. 2. Да, воспользуйтесь оператором catch для каждого перехватываемого исключения. 3. Исключение, не перехваченное внутренним блоком try/catch, перемещается вовне по направлению к внешнему блоку try.
372 Глава 10. Обработка исключений Результат выполнения программы: Перед использованием оператора throw. Исключение перехвачено. После блока try/catch. Обратите внимание на то, что исключение DivideByZeroException было создано с помощью модификатора new оператора throw. Помните о том, что throw оперирует с объектами. Поэтому, прежде чем использовать этот оператор, требуется подготовить объект. В рассматриваемом случае для создания объекта DivideByZeroException используется конструктор, заданный по умолчанию, однако для создания исключе- ний доступны и другие конструкторы (соответствующий пример приведен далее). ^^-V/пве/пы профессионала—........................ гЁа Вопрос. Почему возникает потребность в обеспечении генерирования исклю- чений самим программистом? Ответ. Такая потребность возникает при использовании исключений из созданных программистом классов исключений. Далее в этой главе будет показано, что создание собственных классов исключений позволяет обрабатывать ошибки в коде в рамках общей стратегии обработки исключений. Повторное генерирование исключения Исключение, перехваченное одним оператором catch, может генерироваться по- вторно, благодаря чему оно может перехватываться внешним оператором catch. Наиболее частой причиной повторного генерирования исключений является обеспе- чение доступа к нему со стороны нескольких обработчиков. Например, один обра- ботчик исключений может обрабатывать один аспект исключения, второй — другой аспект. Для повторного генерирования исключения нужно просто указать ключевое слово throw без имени исключения. То есть необходимо воспользоваться следующей формой оператора throw: throw; Помните о том, что повторно сгенерированное исключение не может быть повторно перехвачено тем же оператором catch. Для повторного перехвата используется следующий оператор catch. Приведенный ниже код иллюстрирует механизм повторного генерирования исклю- чений: // Повторное генерирование исключения. using System; class Rethrow ( public static void genException() ( // Массив numer содержит больше элементов, чем denom. int[] numer = ( 4, 8, 16, 32, 64, 128, 256, 512 ); int[] denom — { 2, 0, 4, 4, 0, 8 }; for(int i-C; i<numer.Length; i++) (
Использование блока finally 3*3 try { Console.WriteLine(numer(i] + ” / " + denom[i] -t ” будет ” + numer[i]/denom[i]); ] catch (DivideByZeroException) { // Перехват исключения. Console.WriteLine("Нельзя делить на ноль!”); } catch (IndexOutOfRangeException) { // Перехват исключения. Console.WriteLine("He найдены совпадающие элементы."); throw; // Повторное генерирование исключения. Повторное генерирование исключения. zlass public sta try { Rethrow.genException (); RethrowDemo { с void Main() { Перехват повторно сгенерированного исключения. catch(IndexOutOfRangeException) { <------ // Повторный перехват исключения. Console.WriteLine("Критическая ошибка "выполнение программы прекращено.’’); В данной программе ошибка деления на ноль обрабатывается локально, с помощью метода genException (), а ошибка превышения границ массива будет сгенерирована повторно. Исключение перехватывается с помощью метода Main (). Минутный практикум 1. Что выполняет оператор throw? 2. По отношению к чему применяется throw: к типам или объектам? 3. Может ли исключение генерироваться повторно после того, как оно было перехвачено? Если да, то какая форма оператора throw используется в этом случае? Использование блока finally Иногда требуется определить блок кода, который будет вызываться в то время, когда еще выполняется блок try/catch. Исключение может спровоцировать ошибку, прерывающую выполнение текущего метода, вызывая его досрочный возврат. Но этот 1. Оператор throw генерирует исключения. 2. Оператор throw применяется по отношению к объектам. Эти объекты должны быть экземплярами корректных классов исключений. 3. Да, укажите оператор throw без исключения.
374 Глава 10. Обработка исключений метод может открыть файл или сетевое соединение, которые должны быть закрыты. Подобные ситуации довольно часто возникают в программировании, и в C# поддер- живается удобный способ их обработки с помощью блока finally. С целью указания блока кода, который вызывается после выхода из блока try/catch, поместите блок finally в конец последовательности try/catch. Общая форма конструкции try/catch, включающей блок finally, показана ниже: try ( // Блок кода, выполняющий мониторинг ошибок 1 catch (ExcepTypel ехОЫ) ( // Обработка исключения ExcepTypel. 1 catch (ЕхсерТуре2 ехОЬ2) ( // Обработка исключения ЕхсерТуре2. 1 finally ( // Код блока finally. } Блок finally будет вызываться независимо от того, появится исключение или нет, и независимо от причин возникновения такового. Таким образом, не важно, завер- шается блок try обычным образом либо возникает исключение, — в любом случае выполняется последний блок кода, определенный ключевым словом finally. Ниже приведен пример использования блока finally. // Использование блока finally. using System; class UseFinally { public static void genException(int what) { int t; int[1 nums - new int[2]; Console. WriteLine ("Получение ’’ + what); try ( switch(what) { case 0: t = 10 / what; // Ошибка деления на ноль, break; case 1: nums[4] — 4; // Ошибка выхода индекса за границы массива, break; case 2: return; // Возврат из блока try. } } catch (DivideByZeroException) { // Перехват исключения. Console.WriteLine("Нельзя делить на ноль'"); return; II Возврат из блока catch.
Более близкое знакомство с исключениями 375 catch (IndexOutOfHangeException) { //' Перехват исключения. Console.WriteLine("Не найдены совпадающие элементы."); finally { 1 Вызывается независимо от блоков try /catch. Console.WriteLine("Выход из блока try.’’); ,uss FinallyDemo { public static void Main() { for(int i-0; i < 3; i++) { UseFinally . genException (i} ; Console.WriteLine(); Результат выполнения программы: Получение О Нельзя делить на ноль! Выход из блока try. i'.c...учение 1 Не найдены совпадающие элементы. Выход из блока try. Получение 2 Выход из блока try. Как показывает результат выполнения программы, независимо от того, как был осуществлен выход из блока try, вызывается блок finally. Более близкое знакомство с исключениями Ло настоящего времени производился перехват исключений, но с самими объектами исключений нс выполнялись никакие действия. Ранее уже говорилось, что конструк- ция catch позволяет указывать тип исключения и объект исключения. Все исклю- чения наследуются из класса Exception. В классе Exception определяется множество свойств. Три наиболее интересных из них называются Message, StackTrace и TargetSite. Все они предназначены только Для чтения. Свойство Message содержит строку, описывающую природу ошибки. Свойство StackTrace содержит строку, включающую стек вызовов, предшествую- щих исключению. Свойство Targetsite возвращает объект, который указывает метод, генерирующий исключение. В классе Exception также определяется несколько методов. Наиболее часто исполь- зуемым является метод ToStringO, который возвращает строку, описывающую исключение. В частности, метод ToStringO вызывается автоматически в случае, когда исключение отображается с помощью метода WriteLine О .
376 Глава io. Обработка исключений Следующая программа демонстрирует применение описанных свойств и метода: // Использование членов класса Exception. using System; class ExcTest ( public static void genException() { int [ ] nums -= new int[4]; Console-WriteLine("Перед тем как исключение будет сгенерировано.’’); // Генерирование исключения выхода индекса за пределы диапазона, nums[7] - 10; Console .WriteLine (’’Этот текст не отображается"); F } class UseExcept { public static void Main() { try { ExcTest.genException(); } ; catch (IndexOutOfRangeException exc) { // catch the exception Console.WriteLine("Стандартное сообщение: "); Console.WriteLine (exc) ; // Вызов метода ToStringO Console.WriteLine("Трассировка стека: ” t exc.StackTrace); Console .WriteLine ("Сообщение: ’’ t exc .Message) ; Console .WriteLine ("Targetsite: ’’ + exc. Targetsite) ; } Console. WriteLine ("После оператора catch.’’); F F В классе Exception определяются четыре конструктора. Два из них будут рассмот- рены в дальнейшем: Exception() Exception(string str) Первый конструктор является конструктором, заданным по умолчанию. Второй указывает свойство Message, связанное с исключением. При создании собственных классов исключений требуется реализовывать оба конструктора. Часто используемые исключения В пространстве имен System определяются многие стандартные встроенные исключе- ния. Вес они наследуются из класса Systerr.Exception, а также генерируются CLR в случае возникновения ошибок в процессе выполнения. Многие из наиболее часто используемых стандартных исключений, определенных в С#, представлены в табл. 10.1-
Наследование классов исключений 3 Таблица 10.1. Наиболее часто используемые исключения, определенные в пространстве имен System Исключение Значение ArrayTypeMismatchException DivideByZeroException IndexOutOfRangeException I n val idCastException OutOfMemoryException OverflowException StackOverflowException Тип сохраненного значения несовместим с типом массив Предпринята попытка деления на ноль. Индекс классива выходит за пределы диапазона. Некорректное преобразование в процессе выполнения. Вызов new был неудачным из-за недостатка памяти. Переполнение при выполнении арифметической операциг Переполнение стека. ® Минутный практикум Тэд ’• Что содержит свойство Message? д 2» 2. Когда вызывается код из блока finallv? 3. Каким образом отображается трассировка событий, предшествовавших воз- никновению исключения? Наследование классов исключений Несмотря на то что встроенные исключения C# обрабатывают большинство распро- страненных ошибок, механизм обработки исключений C# не ограничивается этими ошибками. На самом деле мощь подхода C# к обработке исключений проявляется в способности к обработке исключений, заданных программистом. Можно создавать заказные исключения, выполняющие обработку ошибок в пользовательском коде. Генерирование исключений не представляет особых сложностей. Просто определите класс, наследуемый из класса Exception. В качестве общего правила руководствуй- тесь тем, что определенные пользователем исключения наследуются из класса Ap- г. LicationException, так как они представляют собой иерархию зарезервированных исключений, связанных с приложениями. Наследуемые классы нс нуждаются в фактической реализации в каком-либо виде, поскольку само их существование в системе типов данных позволяет воспользоваться ими в качестве исключений. Создаваемые пользователем классы исключений автоматически получают доступные для них свойства и методы, определенные в классе Exception. Конечно, можно переопределять один или больше членов в создаваемых вами классах исключений. Далее будет рассмотрен пример, в котором создается исключение NonlntResultEx- teption, определяющее два стандартных конструктора, а также переопределяющее Метод ToString(). • I Использование заказного исключения, оsing Systern; I. Свойство Message содержит текст, описывающий исключение. 2. Блок finally является последней конструкцией, вызываемой после выхода из блока try. 3. Для вывода на экран трассировки стека отобразите значение свойства StackTrace, опреде- ляемого в классе Exception.
378 Глава 10. Обработка исключений / / Создание исключении . ________ class NonlntResultException : ApplicacionException { <...—| Заказное исключение. // реализация стандартных конструкторов. oublic NonlntResultException() : base() { } public NonlntResultException(string str) : baseistr) { } // Пс'лч пределение ToS tring для Nonin tRe suit Except ion .___ pub_s override string ToStringO { •<———--------1 Переопределение метода ToStringp^ return Message; } } class JustomExceptDemo { pur de static void MainO { Здесь numer содержит некоторые нечетные значения. щ-Д! numer { 4, 8, 15, 32, 64, 127, 256, 512 }; ..nt П denom { 2, 0, 4, 4, C, 8 }; for(int i-0; ienumer.Length; i++) { u'r^z 1 f—-----1 Генерирование заказного исключения. I if ( (numer [i ] %2) 0) I------— -----——------------——I throw new у NonlntResultException("Результат ” + numer[i] + ” / " r denom [ij + ” нечетный.”); Console.WriteLine(numer[i] +-"/’’ + denom [ i ] i " будет ” -r numer[i]/denom[ 1 ] ) ; catch (DivideByZeroException) { // Перехват исключения. Console . WriteLine (’’Нельзя делить на ноль!”); } catch (IndexOutOfRangeException) { // Перехват исключения. Console.WriteLine(”He найдены совпадающие элементы.”); } catch (NonlntResultException exc) { Console.WriteLine(exc) ; i ) } } Результат выполнения программы: 4/2 будет 2 Нельзя делить на ноль! Результат 15 /4 нечетный. 32 / 4 будет 8 Нельзя делить на ноль! Результат 127 / 8 нечетный. Не найдены совпадающие элементы. Не найдены совпадающие элементы.
Перехват исключений производного класса 379 Перед тем как перейти к дальнейшему изложению, немного поэкспериментируем с программой. Например, попытайтесь превратить в комментарий переопределение метода ToStringO и понаблюдайте за результатами. Либо попытайтесь создать исключение, воспользовавшись заданным по умолчанию конструктором, а затем посмотрите, как C# генерирует это исключение в качестве сообщения, заданного по умолчанию. Перехват исключений производного класса При попытках перехвата типов исключений, относящихся к наследуемому и насле- дующему классу, необходимо проявлять осторожность в случае использования опе- раторов catch. Это связано с тем, что конструкция catch для наследуемого класса совпадает с такой же конструкцией для любого наследующего класса. К примеру поскольку наследуемым классом для всех исключений является класс Exception, следовательно, конструкции catch из класса Exception будут перехватывать все возможные исключения. Конечно, использование конструкции catch без аргументов обеспечивает более «прозрачный» способ для перехвата всех исключений, чем опи- сывалось ранее. Однако проблема перехвата исключений наследующих классов является весьма важной в другом контексте, особенно когда пользователь создает свои собственные исключения. Если требуется перехватывать исключения в наследуемом и наследующем классах одновременно, в последовательности catch наследующий класс будет первым. Если же этого не сделать, конструкция catch из наследуемого класса будет также выпол- нять перехват во всех наследующих классах. Этого правила следует придерживаться, ибо помещение наследуемого класса первым приведет к созданию недостижимого кода, когда конструкция catch из наследующего класса никогда не будет вызываться. В C# недостижимая конструкция catch приведет к появлению ошибки. В следующей программе создаются два класса исключений, ExceptA и ExceptB. Класс ExceptA наследуется из класса ApplicationException. Класс ExceptB наследуется из класса ExceptA. Затем эта программа генерирует исключения обоих типов. // Исключения наследующего класса должны следовать перед исключениями наследуемого класса. using System; // Создание исключения. class ExceptA : ApplicationException [<— public ExceptA() : base() { } public ExceptA(string str) : base(str) { } Наследование исключения. public override string ToStringO { return Message; } // Создание исключения, наследующего исключение ExceptA class ExceptB : ExceptA { •<—.......... —...- —-1 Наследование исюГючения из ExceptA^ public ExceptB() : base() { } public ExceptB(string str) : base(str) { }
380 Глава 10. Обработка исключений public override string ToStringO ( return Message; class OrderMatters { public static void Main() { for(int x 0; x < 3; x+ + ) { try { if(x==0) throw new ExceptA("Перехвачено исключение ExceptA"); else if(x—1) throw new ExceptB("Перехвачено исключение ExceptB”); else throw new Exception(); catch (ExceptB exc) { ◄--- // Перехват исключения. Console-WriteLine(exc); } catch (ExceptA exc) { ◄— // Перехват исключения. Console-WriteLine(exc); } catch (Exception exc) { Ч- Console-WriteLine(exc); Вопрос. Поскольку исключения обычно проявляются в виде какой-либо ошибки, почему так важен перехват исключений наследуемого класса? Ответ. Конструкция catch, перехватывающая исключения наследуемого клас- са, позволяет организовать перехват целой категории исключений, обеспечить их обработку с помощью одного оператора catch, а также избежать дублирования кода. Например, можно создать набор исключений, описывающих аппаратные ошибки некоторого рода. Если обработчик исключений просто сообщает пользователю о том, что произошли аппаратные ошибки, можно воспользоваться общей конструкцией catch с целью перехвата всех исключений данного типа. Обработчик может просто отображать строку Message. Поскольку используемый в данном случае код будет одинаковым для всех исключений, одна конструкция catch может обрабатывать все аппаратные исключения. Результат выполнения программы будет таким: Перехвачено исключение ExceptA Перехвачено исключение ExceptB System.Exception: Exception of type System.Exception was thrown at OrderMatters.Main() Обратите внимание на порядок следования операторов catch. Этот порядок является существенным. Поскольку исключение ExceptB наследуется из исключения Ex- ceptA, оператор catch для исключения ExceptB должен следовать прежде оператора
Перехват исключений производного класса 381 catch для исключения ExceptA. Аналогично, оператор catch для класса Exception (который является наследуемым классом для всех исключений) должен отображаться последним. Если вы хотите проверить это утверждение на практике, попытайтесь переставить местами операторы catch. В результате произойдет ошибка компиляции. Проект 10-1. Добавление исключений в класс Queue 0 QExcDemo.cs В ходе осуществления этого проекта будут созданы два класса исключений, которые могут использоваться классами очередей, разработанными в проекте 9-1. Эти классы отображают ошибки состояний, свидетельствующие о том, что очередь пуста или заполнена. Данные исключения вызываются методами put () и get О в случае воз- никновения соответствующих ошибок. С целью упрощения эти исключения в раз- рабатываемом проекте включены лишь в один класс FixedQueue, но пользователь может легко включить исключения в другие классы очередей из проекта 9-1. Пошаговая инструкция I. Создайте файл QExcDemo. cs. 2. В файле QExcDemo.cs определите следующие исключения: /* Проект 10-1 Добавление обработки исключений в классы очередей. ж I using System; !! Исключение для ошибок заполнения очереди. class QueueFullException : ApplicationException { public QueueFullException() : base() { } public QueueFullException(string str) : base(str) { } public override string ToStringO { return ”\n” + Message; } // Исключение для ошибок пустой очереди. class QueueEmptyException : ApplicationException { public QueueEmptyException() : base() { } public QueueEmptyException(string str) : base(str) { } public override string ToStringO { return ”\n” + Message; } } Исключение QueueFullException генерируется в случае, если предпринимается попытка сохранения элемента в уже заполненной очереди. Исключение Queue- EmptyException генерируется в случае, если предпринимается попытка удаления элемента из пустой очереди.
382 Глава 10. Обработка исключений 3. Измените класс FixedQueue таким образом, чтобы он генерировал исключение ж в случае возникновения ошибки, как показано ниже. Добавьте его в класс г QExcDemo.es. // Класс символов фиксированного размера, использующий исключения. class FixedQueue : ICharQ { char[] q; .'/ Этот массив содержит очередь, int putloc, getloc; // Индикаторы put и get ;/ Конструирование пустой очереди заданного размера. public FixcoQueue(int size) { q = new char(size+1]; // Выделение памяти для очереди. putloc getloc - 0; } /! Помещение символа в очередь. public void put(char ch) { if (putloc—q. Length-1) throw new QueueFullException (’’Максимальная длина ” + (q.Length-1)); putloc-*--»-; q[putloc] - ch; // Получение символа из очереди, public char get() { if(getloc =- putloc) throw new QueueEmptyException(); getloc+-r; return q[getloc]; Благодаря добавлению исключений в класс FixedQueue можно обрабатывать ошибки очереди наиболее рациональным способом. Как вы помните, предыдущая версия FixedQueue просто отображала сообщение об ошибке. Генерирование исключений более эффективно, поскольку в этом случае код, использующий класс FixedQueue, может еще и обрабатывать ошибки. 4. Для проверки возможностей обновленного класса FixedQueue просто добавьте указанный ниже код класса QExcDemo в файл QExcDemo.cs. // Демонстрация возможностей исключений, относящихся к очереди. class QExcDemo { public static void Main() { FixedQueue q = new FixedQueue(10); char ch; int i; try { II Переполнение очереди. for(i-0; i < 11; i++) { Console.Write ("Попытка сохранения.- *' + (char) ( ’A’ + i) ) ; q.put((char) ('A’ + i) ) ; Console . WriteLine (’’ OK”);
Использование ключевых слов checked и unchecked 383 } Console.WriteLine(); 1 catch (QueueFullException exc) { Console.WriteLine(exc); } Console.WriteLine (); try ( // Попытка чтения из пустой очереди. for(i-0; i < 11; i++) [ Console.Write("Получение следующего символа: "); ch - q.get () ; Console.WriteLine(ch); } } catch (QueueEmptyException exc) { Console.WriteLine(exc) ; } } 5. Для создания программы потребуется скомпилировать файл QExcDemo. cs совме- стно с файлом lQChar.cs. Файл lQChar.cs содержит код интерфейса очереди. При запуске на выполнение программы QExcDemo получается следующий результат: Попытка сохранения: А - ок Попытка сохранения: В - ок Попытка сохранения: С - ок Попытка сохранения: D - ок Попытка сохранения: Е - ок Попытка сохранения: F - ок Попытка сохранения: G - ок Попытка сохранения: Н - ок Попытка сохранения: I - ок Попытка сохранения: J - ок Попытка сохранения: К Максимальная длина 10 Получение следующего символа: А Получение следующего символа: В Получение следующего символа: С Получение следующего символа: D Получение следующего символа: Е Получение следующего символа: F Получение следующего символа: G Получение следующего символа: Н Получение следующего символа: I Получение следующего символа: J Получение следующего символа: An exception of type QueueEmptyException was thrown Использование ключевых слов checked и Unchecked В ходе выполнения арифметических вычислений могут возникать ошибки перепол- нения. Обратите внимание на представленный ниже пример.
384 Глава 10. Обработка исключен^ byte a, b, result; а - 127; b - 127; result - (byte)(а ” b) В этом примере произведение множителей а и b превышает допустимый диапазон значения byte. В результате произошла ошибка переполнения. В C# можно задавать вызов исключений в случае переполнения, воспользовавшись ключевыми словами checked и unchecked. Для указания выражения, проверяемого в случае переполнения, используется ключевое слово checked. Для игнорирования переполнения используется ключевое слово unchecked. В последнем случае проис- ходит усечение результата с тем, чтобы тип результата совпадал с типом целевого выражения. Оператор checked имеет две основных формы. Одна форма используется для проверки указанного выражения. Вторая форма используется для проверки блока операторов. checked (ехрг) checked { t / Проверяемые операторы. В этом фрагменте кода параметр ехрг определяет проверяемое выражение. Если при вычислении выражения возникает переполнение, генерируется исключение Over- fl owException. Оператор unchecked имеет три основных формы. При использовании первой формы игнорируется переполнение для указанного исключения. Вторая форма игнорирует переполнение для блока операторов. unchecked (ехрг) unchecked { // Операторы, для которых будет проигнорировано переполнение- В этом фрагменте кода параметр ехрг определяет выражение, которое не будет проверяться на предмет возникновения ситуации переполнения. Если при вычисле- нии подобного выражения возникает ситуация переполнения, происходит усечение результата. Приведенная ниже программа демонстрирует механизм использования ключевых слов checked и unchecked. // Механизм использования ключевых слов checked и unchecked. us trig System; class CheckedDemo { public static void MainO { byte a, b; byte result; a - 127; b - 127;
использование ключевых слов checked и unchecked 385 Ре nvibiar, приведшим к переполнению, усеклося j rcsd_L ur.checxca ( (cy~e) (a * eh; / ci.sо- e . W r » r e Line (” Licilp q&epя<•.мьи neл^сас : " t r. >_ s u - i ;___ ^^__---|TicpciiojjneHiic приводи! к «снсрировлнию исключения . resu-L uhec; <ed ( (cyie) ia ’ h; i ' Jpi-ia .дни* .... ч kc.-j.icmc .сж J с й У v _ e . /’ r _ z e L r 11 e ( ” Il f > с в e p я e мьд: резтл b t’j r: " • :. t s u _ L } ; / n о mc >i; с n (Over t 1owExcep: ton exe) Pc>\ ibfai выполнения программы: Hl. . геряомый результат: S \ ‘ - О ч' с г 11 о w Е р z 1 о п 1 Е х с ер l д е д ? г с у о е в с С pi с с е 2. Г11? .т. о. Гсй i. I’; () В \о1с выполнения программы pen;iLiai вычисления непроверяемою выражения ycek.ieiCH. Переполнение, вызываемое проверяемым выражением, привозит к гене- рированию исключения. В :.релыдутсй программе показано использование ключевых слов спи-с.<са и ~ с: • ...з в составе единственного выражения. В следующей программе демонсгри- рхючея проверка и отсутствие проверки для блока операторов: ; ‘..л: с ль зов анхе ключевых слоз uhec.ceci и unchecked пези раоо’лс с {элсхами ’’рдт’Сров. «ь . Зузсе.т.; in che eked b 5; result; - unchecked ( (by ce) (a * cn); Consc-le . Wr r teLine ('Непроверяемый рез\ ль-^аи :
386 Глава 10. Обработка исключ! result - checked((оуte)(а * о)); // Все в порядке. Console.WriteLine("Проверяемый результат: " + resu 127; 127; ': Не может // выполняться. ch (OvertlowException / Перехват исключения. Результат выполнения программы: Непроверяемый результат: 1 Непроверяемый результат: 113 Проверяемый результат: 14 Sustem.OverflowException: Exception of type System.OverflowException was thrown, at ChackedBlocks.Main() Как видите, в случае возникновения переполнения в непроверяемых блоках про- изводится усечение результата. Если же переполнение происходит в проверяемом блоке, генерируется исключение. 1 Единственная причина, в силу которой требуется использовать ключевые слова checked и unchecked, заключается в том, что статус переполнения проверяемый/не- провсряемый определяется путем установки соответствующей опции компилятора, а также с помощью самой среды выполнения. Поэтому в некоторых программах лучше явно указать статус проверки переполнения. ...— •>..................- — О Вопрос. Когда следует использовать обработку исключений в программах? Когда нужно создавать пользовательские, заказные классы исключений? Ответ. Поскольку в C# повсеместно применяются исключения е целью сообщения о возникающих ошибках, практически все прикладные программы ис- пользуют механизм обработки исключений. Обработка исключений, основанная на использовании встроенных исключений С#, является наиболее простой для освоения начинающими программистами. Труднее принять решение относительно того, когда и как следует использовать пользовательские, настраиваемые исключения. Сущест- вуют два общих способа создания отчетов о возникающих ошибках: возвращаемые значения и исключения. Какой же подход лучше? Несомненно, при программирова- нии на языке C# использование механизма обработки исключений должно быть нормой. Хотя возврат кода ошибки и может стать альтернативой в некоторых случаях, исключения обеспечивают более совершенный и более наглядный способ обработки ошибок. Именно этот способ должны выбирать профессиональные программисты на С#, выполняющие обработку ошибок в своих программах.
387 контрольные вопросы Контрольные вопросы 1. Какой класс находится на вершине иерархии исключении? 2. Кратко опишите методику использования операторов try и eaten. 3. Найдите ошибку в следующем фршментс кода: // ... vals[18] 10; catch (IndexOutOfRangeException exc) ( И Обработка ошибки. } 4, Что произойдет, если исключение не будет перехвачено? 5. Найдите ошибку н следующем фрагменте кода: class А : Exception { ... class В : А , ... try { // ... } catch (A exc) . ... , catch (В exc) { ... ) 6. Может ли исключение, перехваченное внутренним блоком eaten, по- вторно генерироваться в качестве исключения для внешнего блока catch? 7. Является ли блок finally последним фрагментом кода, выполняемым перед завершением программы? Обоснуйте свой ответ. 8. В упражнении 3 раздела «Контрольные вопросы» 1лавы 6 был создан класс stack. Добавьте в этот класс заказные исключения, которые сообщают о состояниях пустого и заполненного стека. 9. Расскажите о назначении ключевых слов checked и unchecked. 10. Каким образом могут быть перехвачены все исключения?
: Ввод/вывод □ Поток □ Классы потоков □ Консольный ввод/вывод □ Ввод/вывод в файлы CJ Чтение и запись двоичных данных О Произвольный доступ к содержимому файла О Преобразование числовых строк
389 |-|0токи ввода/вывода C# q самою начала этой книги в иршраммах иенользусtea нодсиосма ввода/вывода Cs „ . г eLi • Однако пока она не была че1КО описана. Поскольку при виоле 'выводе и С₽ используется иерархия классов, невозможно представить теорию !, гегали этой подсистемы без предварительного описания классов, механизма наследования и исключении. Теперь мы готовы к изучению этой >смы. Как отмеча- оеь в 1лавс 1, в СР используются подсистема ввода/вывода и классы, определенные н среде .NET l-ramcwork. Таким образом, описывая подспсмсму ввода/вывода в Сд, viu Тактически описываем подсистему ввода/вывода платформы .NET. В ; две рассматриванием механизмы консольною ввода/вывода и ввода/вывода в файлы. Следует учесть, что подсистема ввода/вывода в Ср являеия весьма развитой. Bi 1,’i.e рассматриваются наиболее важные и широко используемые возможпоС1и мой по юис1емы, однако некоторые аспекты использования механизма ввода/вывода при злея изучать самосюятсльно. К тчаегыо, подсис1сма ввода/вывода С;: довольно доючна. Как только вы поймете основы мои подсистемы, освой гь де гали cranei значшельно проще. Потоки ввода/вывода C# Программы C# реализуют ввод/вывод с помощью потоков. Под потоком понимается вывод либо получение информации. Поток связан с физическим устройством по- средством системы ввода/вывода С#. Все потоки ведут себя одинаково, в то время как реальные физические устройства, которые с ними связаны, существенно отли- чаются друг от друга. Поэтому классы и методы ввода/вывода могут применяться совместно с устройствами многих типов. Например, методы, используемые для вывода на консоль, могут также использоваться для записи данных в‘файл надиске. Байтовые потоки и потоки символов И., самом низком уровне все операции ввода/вывода в Ср оперируют байтами. Пз. юбный подход имеет смысл, поскольку большинство устройств, предназначенных Д-ь выполнения операций ввода/вывода, являются байт-орнентированпыми. Однако людям при общении с компьютером удобнее использовать символы. Напомним, что в 'тип данных char имеет разрядность 16 бигов, а тип данных ь-с- является пювым. Если применяется набор спхзволов ASCII, довольно просто выполнить ||Г"образованис между типами ег.зг и оус при этом для величины i и па -спа: нужно 11 ’’орировать старший байт. Данный прием не подходит при работе с остальными Символами Lnicodc, которые используют оба баша. Полому байтовые потоки нс Клэппе удобно использовать при выполнении символьною ввода,, вывода. Для решс- тгои задачи в Ср определены несколько классов, преобразующих байтовый поток 4 о ток символов, то есть автоматически преобразующих данные типа суг.е в данные 1.1 и наоборот. Предопределенные потоки 1 и предопределенных потока прсдс-.в 1сны свопсгвахш '. г . - -. -)ги потоки дос'-II- '.1ч всех iipoipaxiM. испо.1ьзу1оишх просгран- зо к ' .1 Ноток ; coomciciBvci cT.m.'t.-ipnioMx потоке вывода.
Глава 11. Ввод/выв( 'молчанию это консоль. Так. при вызове метода Console .WriteLine () информ ия автоматически направляется в поток Consol о .Out. Поток Console . In связан? згандартны.м устройством ввода, которым по умолчанию является клавиатуру I3KO эти потоки могут быть перенаправлены на любое соответствующее устройство ца/вывода. Стандартные потоки являются символьными потоками. Поэтому оНц ' тыкают и записывают символы. , \ Минутный практикум К I. Что в C# понимается под потоком? У ’Л 2. Назовите типы потоков С#. " 3. Какие потоки являются предопределенными? л 1ССЫ потоков I бг определяются классы байтовых потоков и классы потоков символов. Однако® тссы потоков символов в действительности представляют собой оболочку, котора»Д| собразует передаваемый на более низком уровне поток байтов в поток символов/» гичем выполнение этого преобразования происходит автоматически. Таким обраСж м, символьные потоки построены на основе байтовых потоков и являются и|9 гическим дополнением. ;Л :е классы потоков определены в пространстве имен System. ТС. Для применения» их классов в начале программы обычно помешают следующий оператор: inc; System. IO; ет необходимости в указании класса System..10 для консольного ввода/вывода.® ласе Console определен в пространстве имен System. Й iacc Stream | отоки в C# создаются с помощью класса System. ТО. Stream. Класс Stream представ- |. ют байтовые потоки и является базовым классом для всех других классов потоков. Он | гкже является абстрактным, поэтому не сушествует реализации объекта stream. Класс'Д tre3n- определяет набор стандартных операций с потоками. В табл. 11.1 описано г, ссколько распространенных методов, определенных в классе Stream, -J. 4 аблица 11.1. Некоторые методы, определенные в классе Stream Иетод Описание /Old Closet) Закрывает поток /ord Flush() Записывает содержимое потока в физическое устройство int ReadByreO Возвращает целочисленное представление следующего доступного байта в потоке ввода. По достижении конца файла возвращает -1 — ——” I- Под потоком в Ctt понимается вывод либо получение данных. - В C# есть два типа потоков — байтовые и потоки символов. 3. Предопределенными потоками являются потоки Console. In, Console - Out и Console . Error-
Классы потоков 391 Метод Описание , Read (byte [] buf, Осуществляет попытку чтения numBytes байтов из потока , ;1t offset, int numBytes) ввода и помещает их в массив buf, начиная с элемента but [ offset], При этом возвращается количество успешно считанных байтов i,q Seek (long Offset, SeekOrigin origin) Задает позицию текущего байта в потоке, используя значе- ние смещения offset относительно позиции origin . WriteByte(byte b) Осуществляет запись одного байта в поток вывода Write (byte IJ buf, ,t offset, int numBytes) Осуществляет запись numBytes байтов в поток вывода из массива buf, начиная с элемента buf] offset]. При этом возвращается количество записанных байтов Как правило, если появляется ошибка ввода/вывола, методы, приведенные в табл. I l.l, генерируют исключение lOException. Если предпринимается попытка выполнения недопустимой операции (например, попытка записи информации в поток, предна- значенный только для чтения), генерируется исключение HotSupportedExcepti.cn. Обратите внимание на то, что в классе Stream определены методы, предназначенные для записи и чтения данных. Однако не все потоки поддерживают и запись, и чтение, поскольку можно открывать потоки, предназначенные только для чтения или только дзя записи. Также не все потоки поддерживают позиционирование методом Seek о . С целью определения возможностей конкретного потока используется одно или несколько свойств класса Stream. Эти свойства приведены в табл. 11.2. Таблица 11.2. Свойства, определенные в классе Stream Свойство Описание boor CanRead Свойство имеет значение true, если возможно чтение из потока. Свойство предназначено только для чтения bool CanSeek Свойство имеет значение true, если поток позволяет задавать теку- щую позицию элемента. Свойство предназначено только для чтения ooul CanWrite Свойство имеет значение true, если возможна запись в поток. Свой- ство предназначено только для чтения ieng Length Свойство указывает длину потока. Свойство предназначено только для чтения lang Position Свойство представляет позицию текущего элемента потока. Свойство предназначено как для чтения, так и для записи Классы байтовых потоков иi класса Stream ; наследуются три класса байтовых потоков, перечисленные ниже. Класс Описание 'rif feredStream Обеспечивает буферизацию байтового потока. В большинстве случаев буферизация позволяет увеличить производительность > LeStream Байтовый поток, предназначенный для осуществления операций вво- да/вывода в файл kemoryStream Байтовый поток, использующий память в качестве хранилища
392 Глава 11. Ввод/выв, Программист может определять и собственные классы потоков. Однако для подав-',1 лающего большинства приложений вполне достаточно встроенных потоков. < Классы потоков символов В верхней части иерархии символьных потоков находятся абстрактные классы й и Textwriter. Методы, определенные в этих классах, обеспечиваютД минимальный набор функций ввода/вывода для всех символьных потоков. J В табл. 11.3 приведены методы ввода класса TextReader. Обычно в случае возник- j| новения ошибки эти методы генерируют исключение lOException. (Иногда возмож- л но возникновение других исключений.) Особый интерес представляет метод Read-’® .—г.е (), который полностью считывает строку текста, возвращая се в виде строки C# '$, (данных типа string). Этот метод удобно использовать при чтении данных из потока Ж ввода, содержащих пробелы. 1 ®. Таблица 11.3. Методы ввода, определенные в классе TextReader Метод Описание void Closed Закрывает поток ввода int ?eek() Получает следующий символ из потока ввода, но не удаля-у ет его. Возвращает -1, если нет доступного символа J int Read() Возвращает целочисленное представление следующего.*' доступного символа из потока ввода. Возвращает -1 efy случае достижения конца потока (1 int Read (char[] but, Осуществляет попытку чтения numChars символов из по-т int Offset, тока ввода и помещает их в массив buf, начиная с элемента j int numchars') but [offset], При этом возвращается количество успешно^ считанных символов -•'] int ReadBlock(char[ ] buf, Осуществляет попытку чтения numChars символов из по- int offset, тока ввода и помещает их в массив buf, начиная с элемента > int numChars) Ш [ offset]. При этом возвращается количество успешно ; считанных символов string Readlinet) Считывает строку текста и возвращает ее в виде строки С#. * Нулевое значение возвращается в том случае, если был считан символ конца файла string ReadToEndO Считывает все оставшиеся символы потока и возвращает", их в виде строки C# В классе TexrWriter определены различные версии методов Write,') и Write- / Tib(). которые позволяют выводить данные встроенных типов. Ниже приводится j лишь несколько перегруженных версий этих методов. it Метод Write (int Val) Write (double val) id iteLi ne(string val) d Wr nefuint val) WriteLine(char val) Описание Запись значения типа int Запись значения типа double Запись значения типа bool Запись строки, завершаемой символами перехода на но- вую строку Запись значения типа uint и символов перехода на но- й вую строку ,1 Запись символа, за которым следуют символы перехода 1 на новую строку j Ж d i d
консольный ввод/вывод 393 р дополнение к методам Write О и WriteLine О в классе Textwriter также определены методы Close о и Flush о : .-uai void Close О , . •ual void Flush() Метод F’lusr. О используется для вывода на физический носитель данных, которые осилпсь в буфере вывода. Метод Close () закрывас! поюк. Классы Телтйеабет и TexiWutet реализуются с помощью перечисленных ниже к lauCOB символьных потоков. Поэтому данные потоки поддерживают методы и свойства, определенные В классах Text Redder И TextWriter. Класс потока S;. г aasiReader S; eamWriter St' ngReader St i ngWrrter Описание Считывает символы из байтового потока. Этот класс является оболоч- кой байтового потока ввода Записывает символы в байтовый поток. Этот класс является оболочкой байтового потока вывода Считывает символы из строки Записывает символы в строку Двоичные потоки В дополнение к байтовым и символьным потокам в C# определяются два двоичных класса потоков, которые могут использоваться для непосредственного считывания и записи двоичных данных. Эти потоки называются BinaryR-tader и Bir.aryWriter. Они подробно рассматриваются далее в этой главе при описании механизма вво- да 'вывода в двоичные файлы. Теперь, когда вы получили общее представление о структуре подсистемы ввода/вы- нода в С#, приступим к детальному изучению компонентов этой структуры. Сначала расмотрим подсистему консольного ввода/вывода. Минутный практикум 1. Какой класс находится в верхней части иерархии потоков? Э 2. Назовите свойства класса Stress:. '' 3. Какие классы находятся вверху иерархии классов символьных потоков? Консольный ввод/вывод консольный ввод/вывод реализуется с помощью стандартных потоков ?с .-.за 11. in, •le.Out и console. Error. Консольный ввод/вывод рассматривался в главе I. 'юному вы с ним уже знакомы. Как будет показано далее, этот ввод/вывод обладает "екогорыми дополнительными возможностями. *• В верхней части иерархии потоков находится класс stream. Это следующие свойства: CanSeek, CanRead, CanWrite, Length и Position. 3 Вверху иерархии классов символьных потоков находятся классы TextReaaer и Textwrrter.
394 Глава 11. Ввод/выв( Однако, прежде чем приступить к изложению дальнейшего материала, елсдует еще* раз подчеркнуть следующий момент: большинство реальных приложений C# цдК являются консольными приложениями, ориентронанпыми на обработку тскстаЯ В данном случае речь идет скорее о iрафических программах либо компонентах,’ использующих оконный интерфейс для организации взаимодействия с пользовате» ж лем. Поэтому в Сопрограммах не столь широко применяется консольный ввод/вы- ® вод. Хотя программы, имеющие интерфейс в виде текстовой строки, представляют Ж собой прекрасные учебные примеры и могут пригодиться в качестве компактных”® утилит, они не подходят в качестве реальных коммерческих программ. • > Чтение данных с консоли я Поток Console.In является экземпляром класса ?extp*?der и для получения.# доступа к нему предназначены методы и свойства, определенные в классе Тех-Ж tReader. Однако обычно используются методы класса console, с помощью которых* выполняется автоматическое считывание данных из потока console. In. В классе ж Console определяются два метода ввода: Pead () и ReadLine (). ж Для чтения одного символа применяется метод Read () . ж static int. Read () Этот метод мы использовали в главе 3. Метод возвращает очередной символ^Ж считанный с консоли. Возвращаемый символ имеет тип int, и требуется егож преобразование в тип char. При возникновении ошибки возвращается значение -1,Ж Если возникает сбой, метод генерирует исключение icExceptton. Ж Для считывания строки символов используется метод peadLine (): static string ReadLine() £ Этот метод считывает введенные символы до тех пор, пока не будет нажата клавиша ж [Enter], Считанные символы возвращаются в виде объекта типа string. При сбое Ц. также генерируется исключение lOExcevtton. $ Ниже приводится программа, демонстрирующая процесс чтения массива символов,;^: из потока Console.In. // Ввод с консоли посредством метода ReadLij using System; class ReadChars { public static void Main() { strong str; Console.WriteLine("Введите несколько chi str -= Console. ReadLine (); <—---—— Console.WriteLine("Вы ввели: " + str); } } Результат выполнения программы: Введите несколько символов. Это тест. Вы ввели: Это тест. Чтение С1роки, введенной с клавиатуры. %
КОНСОЛЬНЫЙ ввод/вывод 395 Несмотря на то что методы класса Console реализуют наиболее простой способ чтения потока Console, in, можно вызывать методы, находящиеся на более низком уровне — в классе TextReaaer. Ниже приведен текст предыдущей программы, переписанной с использованием методов, определенных в классе TextReader. ,* непосредственное использование потока Console. In для чтения массива сайтов с клавиатуры. */ us-:g System; cl„iss ReadChars2 { public static void Main() ( string str; Console.WriteLrne("Введите несколько символов.”); str - Console.In,ReadLine() ; <- Непосредственное «пенис из поiока Console.j n. Console.WriteLine("Вы ввели: " + str); Обратите внимание на то, как вызывается метод ReadLine () для потока Console. In. Вывод данных на консоль Потоки Console.Out и Console.Error являются объектами типа TextWr;- ter. Вывод данных на консоль проще всего реализовать посредством уже знакомых вам меюдов Write () и WriteLine (). Различные версии этих методов предназначены для вывода данных каждого из встроенных типов. В классе Console определены собственные версии методов Write () и WriteLine () , поэтому их можно вызвать непосредственно через этот класс. Однако эти (и другие) методы можно вызвать и через класс Textwriter, который находится на более низком уровне. Ниже приводится программа, демонстрирующая выполнение записи в потоки Con- s' с.Gut И Console.Error. Запись данных в потоки Console.Out и Console.Error. j System; ^--uss ErrOut { Public static void Main() ( int a-10, b=0; int result; Console.Out.WriteLine("Генерирование исключения."); try { | result - a ./ b; // Генерирование исключения. : catch(DivideByZeroException exc) { Console. Error. WriteLine (exc .Message) ; *+ Запись в потоки Console.Out И Console . Er ror.
396 Глава 11. Ввод/выт Результат выполнения программы: I'сиерироз j11не ксключеии ч . '№ ПкЛ.ьгека деления на нул^ . ^ЯИ Иногда пользователи. имеющие небольшой опыт и ттрот раммировании. нс мог&да решить, когда следует применять поток гг щ£тг-л Поскольку потоки Corjtw le.Oi.- и Т'г:.' 1,_ .у- • с г по умолчанию осуществляют вывод на консоль, зачеьмК нужны два различных потока? При озвезе на этот вопрос следует учесть тот фавд^Ж что етандттртныс потоки можно перенапрашп ь на лруте устройстиа в во да / в ы во пя~ Ж Например, содержимое потока . .?. : можно направить вместо экрана файл на диске. Iona сообщения об ошибках вывода, например. будут записаны в® журнальный файл, а обычные сообщения прозраммы отобразятся на экране. Кстати^® если вывод на консоль перенаправлен, а поток ошибок нет, то сообщения об ошибках® можно увидеть на консоли во время работы протраммы. Процедура перенаправления®: потоков будет рассмотрена далее. после описания операций ввода/вы ззо.ча в файлы, ж- Класс FileStream и байт-ориентированный Ж ввод/вывод в файлы Ж В C# поддерживаются классы, обеспечптза тощие операции чтсния/записп для файловд| Конечно, наиболее распространенным типом файла является дисковый файл. НЯк уровне операционной сисзсмы все файлы рассматриваются как двоичные фатыы. Казвда и следовало ожидать, в С~ похтерживакнея методы тения и записи байтов прйда работе с файлами. Пол ому достаточно широко распространены операции чтеиия/зд$» писи файлов, при осуществлении которых используются потоки байтов. В С#Ж обеспечивается заключение файлового потока, ориентированного на использование® байтов тз символьный поток. Операции с символьными файлами удобны, если» требуется сохранять текст. Символьные потоки будут описаны далее в этой главе.® Сейчас же мы расскажем о механизме ввода/вывода, ориентированном на пспользо-ж ванне байтов. | • Для создания байтовою потока, связанною с файлом, используется класс File-Д . Сгтещ. Этот класс наследуется из класса ’-.в и поэтому имеет все свойства, '• I присущие классе .ю . у : j . Помните, что классы потоков (в том числе и класс з-_ ; :-3t г- определены В' SycC’-n . IC-. Поэтому обычно тз начало любой программы, применяющей классы потоков, включается конструкция. а. us.ng Щщет.Ю; Д Открытие и закрытие файла | При создании байтовою потока, связанного с файлом, требуется сформировать ф объект г..ё.с- г<-.=..ч. Из этою обьскта наследуется несколько конструкторов. Наибо- я лес часто используется следующий конструктор: Я F; leSei ean: т s:t. ng г?ij>’Нсле ,та-./е) тде I . -,г •- сказывает имя открываемою файла, которое может включать полностью определенный путь к нему. Параметр т-- определяет, каким образом открывается а
1<ласс FileStream и байт-ориентированный ввод/вывод в файлы зэ' файл. Здесь необходимо указать одно из значений, определенных в перечислснш г. ,;Mode. Эти значения описаны в табл. I 1.4. Если попытка открытия файла была неудачной, генерируется исключение. Если фай. нС может быть открыт, поскольку он не существует, генерируется исключснш i:c.tF oundException. Таблица 11.4. Значения перечисления FileMode Значение Описание Fi ,eMode.Append Выводимые данные добавляются после данных, находящихся в файле F; :eMede.Create Создается новый выходной файл. Любой ранее созданный файл с аналогичным именем уничтожается Fi .eMode.CreateNew Создается новый выходной файл F: .eMode.Open Открывается существующий файл Fi.eMode.OpenOrCreate В случае наличия файла происходит его открытие, если файл не существует, он создается Fi i eMode.Truncate Открывается существующий файл, но его длина усекается до ноля Если из-за ошибки ввода/вывода файл не открывается, генерируется исключснш FC:?xceptron. Другие возможные исключения: ArgunientE'ullException (имя файл! выражается нулевым значением), ArgumentException (параметр, задающий режих открытия файла, некорректен), SecurityException (пользователь нс имеет npai доступа) и DirectoryNotFoundException (указанный каталог нс существует). Ниже демонстрируется способ открытия файла test .de: для ввода данных: FilcSLream fin; try ( new Filestream("test.dat", FileMode.Open); } c.i( ch (FileNotFoundException exc) 'Tisole.WriteLine(exc.Message); - curn; ca - { •'•sole .Wri teLine ("Невозможно открыть файл."); ic turn; Здесь первый оператор catch перехватывает ошибку, связанную с нсвозможностьк обнаружения файла. Второй оператор catch обеспечивает перехват всех остальны: исключений, связанных с выполнением файловых операций. Часто имеет смысл орга низовать выдачу сведений о каждой ошибке, что позволит располагать более точным! ведениями о характере возникшего затруднения. Для простоты изложения в рассмат Пинаемых примерах перехватывается лишь исключение FileNotFoendException. Как уже упоминалось, только что описанный конструктор FtleStreart. открывав Фапл для чтения и записи. Если доступ необходимо ограничить только чтением ил, й’лько записью, укажите строку следующего вида: ?~-eStream(string filename, FileMode mode, FileAccess how)
398 Глава 11. Ввод/выврдД! Как и прежде, параметр filename указывает имя открываемого файла, а параметр Ж шоае определяет режим открытия файла. Значение, передаваемое параметром how/’Ж задает споеоб доступа к файлу. Здесь может фигурировать одна из указанных нижа Л величин, заданных перечислением FileAccess: ! Л; FileAccess.Read FileAccess.Write ?,;leAccc5s. ReadWrite t Например, следующая строка открывает файл только для чтения: Ж FileStream fin - new FtleStream(”test.dat", FileMode.Open, FileAccess.Read) •' Если работа с файлом была завершена, необходимо закрыть его, используя метод Close () : void Close() • При закрытии файла происходит освобождение системных ресурсов, выделенных для -! файла. С этого .момента они становятся доступными при обработке другого файла. Метод close () может генерировать исключение lOExceptron. Чтение байтов с помощью класса FileStream Класс FileStream определяет два метода, которые могут считывать байты из файла:» ReadByte() и Fead(). Для считывания одного байта из файла воспользуйтесьИ? методом ReadByte () , общая форма которого приводится ниже: .» int ReadByte () Каждый раз при вызове этого метода из файла считывается один байт, который® возвращается в виде целочисленного значения. Если при этом встречается признак.» конца файла, возвращается значение -I. В случае возникновения ошибки генсриру-; ' ется исключение lOExceptron. Для считывания блока байтов воспользуйтесь методом ReadO : int Read(Dyte[] buf, int offset, int numByces} Метод Read () предпринимает попытку считать байты, количество которых определяется параметром numBytes. из файла в буфер buf. начиная с позиции, заданной параметром offset. При этом возвращается количество успешно считанных байтов. В случае возникновения ошибки ввода/вывода генерируется исключение lOException. Возмож-. ны и другие исключения, в том числе исключение NotSupportedException, генериру- емое в случае, когда операция чтения не поддерживается потоком. В следующей программе используется метод ReadByte () Для ввода и отображения содержимого текстового файла, имя которого указано в качестве аргумента команд-- ной строки. Обратите внимание на то, что блоки try 'catch обрабатывают две ошибки, происходящие при первом запуске программы (указанный файл не найден либо имя файла не указано). Аналогичный подход может применяться всякий раз, когда используются аргументы командной строки. /* Отображение файла. Для использования программы укажите имя файла, содержимое которого будет отображено. Например, для просмотра содержимого файла TEST.CS воспользуйтесь следующей командной строкой: ShowFile TEST.CS
1<ласс FileStream и байт-ориентированный ввод/вывод в файлы 399 u^;ng System; u5i:ig System.IO; c'jtss ShowFile { tuolic static void Main(string[] args) { ,nt i; FilcStrearr. fin; try { tin - new Filestream(args[C], FileMoae.Open) ; catch(FileKotFoundException exc) [ Console.WriteLine(exc.Message); return; catch(IndexOutOfRangeException exc) { Console .WriteLine (exc . Message -t "\nUsage: ShowFile File"); return; i/ Чтение байтов, пока не встретится символ EOF do { try { __________ i = f in . ReadByte () ; --Чтение из файла. } catch(lOException exc) { Console-WriteLine(exc.Message); return; if(i 1- -1) Console.Write((char) i) ; ► while (i -l);-< —j Если i равно - С достигну^ конец файла, fin.Close (); Запись в файл Для записи байтов в файл воспользуйтесь методом WrineByte О : vo.ci WriteByte (byte val) Эии метод записывает байт, указанный параметром мат, в файл. Если при записи возникла ошибка, генерируется исключение LCE-xceptior. Если поток вывода не открыт, генерируется исключение NotSupportedEzception. Байтовый массив может быть записан в файл с помощью метода writ (): iIr- Write (byte [] bufr int offset, int numBytes} Ответь/ ярофеса/мала« СЭ Вопрос. Уже отмечалось, что метод РеазВ/teO возвращает значение -1 по Достижении конца файла, но при этом отсутствует специальное возвращаемое значение в случае возникновения ошибки. Почему? Ответ. В C# ошибки обрабатываются с помощью исключений. Поэтому, если Мсюд ReadByte () либо другой метод ввода/вывода возвращает значение, это озна- чает, что ошибка не произошла. При этом обеспечивается более «прозрачный» путь обработки ошибок ввода/вывода, чем в случае возврата специальных кодов ошибок.
400 Глава 11. Ввод/вывод] Метод WrxtoO записывает в файл количество байтов, указанное параметром num-Я baLes, из массива начиная с позиции, которая задана с помощью параметр./® c::sei. При этом возвращается количество записанных байтов Если в процесс/ Ж записи происходи! ошибка, генерируется исключение i ;<.. Если поток Ж вывода нс тнкрыг, генерируется исключение п-хр.-.т ’..есц хсерт и.т.. Возможно 1 возникновение и других исключений. Я Наверное, вы знаете о том, что при выполнении вывода в файл часто данные не записываются сразу же на физическое устройство. Вместо згою вывод буферизуется > операционной системой до тех пор, пока не накопится достаточно большой фрагмент ’> данных, который сразу же записывается надиск. В результате возрастает производи- | тельность спстехгы. Например, запись данных в файл на диске осуществляется по У секторам, размеры которых варьируются oi 128 байт и больше. Результаты вывода Я программы обычно буферизуются до тс.х пор, пока на диск не может быть записан f? целый сектор Однако если требуется, чтобы данные записывались на физическое устройство независимо от степени заполнения буфера. можно воспользоваться ме- £ годом г 1 „д" (:: s void Flush () Д Если при выполнении этой операции возникает ошибка, генерируется исключение j 1 Э Е х с с р t1 о г.. а Но завершении операции вывода файла необходимо его закрыть, воспользовавшись методом Close1;. При этом lapan шрусюя. чю любые выводимые данные, находя- $ шиеся в буфере, будут записаны н файл. Поэтому нс с тот вызывать метод rijsh() г перед закрытием файла. J В следующем примере выполняемся копирование файла. Имена исходного п целевого - файлов задаются в командной строке. /” Копирование файла. При испо71озевании эюй программы укажите имена исходного =£ и целевого файлов. Нан римс р г для koi: про в а ним ф ай л а Г’ 1RS Г , 7 X 7 Г в файл SECUND. ГХ Г вос.;ольз> к-. е< i глелуллдей кома н Д11 ой с -г роки й: Сору Н 1 е г L Ь.8 Г . ?Х .. SECON Э . > Х7 I
Ввод/вывод в символьные файлы 401 Console.WriteLine(exc.Message + "ХпИсходный файл не найден”); return; // Открытие файла-копии try { font - new FileStream(args[1j, FileMode.Create); I catch(FileNotFoundException exc) { Console.WriteLine(exc-Message + ”\пОшибка создании файла-копии"); return; } catch(IndexOutOfRangeException exc) { Console. WriteLine (exc . Message J- "\nUsage: CopyFile From To"); return; do { i = fin .ReadByte ();-< // Копирование файла try { _______________________________ Считывание байтов из одного файла с последующей их записью в другой файл. if(i != -1) font. writeByte ( (byte) i) ; •< —. J } while(i != -1); } catch(loException exc) { Console.WriteLine(exc-Message + "Файловая ошибка"); fin.Close(); fout.Close(); Минутный практикум I. Что возвращает метод ReadByte () по достижении конца файла? 2. Для чего предназначен метол Flush () ? 3. Какой метод следует вызвать для записи блока байтов? Ввод/вывод в символьные файлы Хотя существуют общие методы обработки байтовых файлов, для этой цели можно акже воспользоваться символьными потоками. Преимущество символьных потоков проявляется в том, что они оперируют непосредственно с символами Unicode. Поэтому, если возникает потребность в хранении текста Unicode, наилучшим выбо- ром в этом случае будут символьные потоки. Как правило, при выполнении файловых шераций, основанных на использовании символов, класс Filestream включается в состав класса StreamReader или Streamwriter. Эти классы обеспечивают авто- матическое преобразование байтовых потоков в символьные и наоборот. I. По достижении конца файла метод ReadByte () возвращает значение -1. 2. Вызов метода Flush () приводит к тому, что данные, находящиеся в буферах вывода, физически записываются на устройство. Для записи блока байтов вызывается метод write ().
Глава 11. Ввод/выво); Помните о том, что на уровне операционной системы файл состоит из набора байтов] Это остается справедливым и для классов StreamReader или Streamwriter. < Класс streamwriter наследуется ИЗ класса Textwriter. Класс StreamReaded наследуется из класса TextReader. В результате классы Streamwriter и StreamJ Reader получают доступ к методам и свойствам, определенным наследуемыми имя классами. !•< Использование класса StreamWriter 5 Для создания символьного потока вывода включите объект stream (напримерд Fi.leStream) в состав класса StreamWriter. Класс Streamwriter определяет НеД сколько конструкторов. Один из наиболее популярных конструкторов показан ниже» Streamwriter(Stream stream) % Здесь параметр stream указывает имя открытого потока. Если указанный поток будеЛ пуст, конструктор вызывает исключение ArgumentException, а если stream равенИ нулю, вызывается исключение ArgumentNullException. Сразу же после создания класс StreamWriter автоматически выполняет преобразование символов в байты. . Ниже приводится код простой утилиты, выполняющей запись информации на диск^ которая считывает строки текста, введенного с клавиатуры, а затем записывает его в файл rest.txt. Считывание текста продолжается до тех пор, пока пользователь не введет слово «stop». Утилита использует класс Filestream, включенный в состав; класса StreamWriter, с целью вывода информации в файл. /* Простая утилита, выполняющая запись на диск, ; которая демонстрирует возможности класса StreamWriter. */ using System; using System.10; class KtoD { public static void MainO I i' string str; Filestream tout; ’ try { 1 Tout new FileStream("test.txt”, FileMode.Create); } I catch(lOException exc) { % Console . Wri teLine (exc. Message + ’’Невозможно открыть файл.”); return ; Создание класса streamWncenj StreamWriter fstr_out new StreamWriter(four); > Console .WriteLine (’’Введите текст stop для выхода/'); < do { Console . Write (" : ”); str - Console.ReadLine(); | t* if (str 1= ’’stop”) { M str •= str ч- ”\r\n"; // Добавление символов образования новой строки. £ Crfstr out.Write (str); <----j Зтшсь строк ,, фай... | V
рвод/вывод в символьные файлы 403 } саLch(lOException exc) { Console . Wri teLine (exc . Message "Файловая ошибка"); return ; while(str "stop"); fair oui.Closel); В некоторых случаях можно открыть файл напрямую, используя класс Stream- к. ' При этом используется один из указанных ниже конструкторов: StreamWriter (string filename) Sere i.i.Writer (string filename, bool appendflag) Здесь параметр filename указывает имя открываемого файла, причем это может быть полностью определенное имя. При использовании конструктора второго вида данные будут добавляться в конец существующего файла, если флагу appendTrue присвоено значение trne. Если же упомянутому флагу присвоено значение false, данные будут замещать содержимое указанного файла. При отсутствии файла в обоих случаях происходит его создание. Также в обоих случаях генерируется исключение lOExcep- ti '' в случае наличия ошибки. Могут генерироваться и другие исключения. Ниже приводится код утилиты, выполняющей запись информации на диск. Код переписан таким образом, что включает класс StreamWriter, выполняющий откры- тие выходного файла. /* Открытие файла с помощью класса StreamWriter. *7 us_ng System; ue?;..ng System. IO; cI-jss KtoD { Г .olic static void Main () { string str; Streamwriter fstr_out; try { fstr_out - new StreamWriter(”test.txt”); } catch(lOException exc) { Console.WriteLine(exc.Message + "Невозможно открыть файл."); return ; } Console.WriteLine ("Введите текст 'stop' для выхода."); do { Console.Write(": "); str - Console.ReadLine(); if (str !=- "stop") ( str = str + ”\r\n"; // Добавление символов образования новой строки, try { fstr_out.Write(str); } catch(lOException exc) {
404 Глава 11. ВворУвывод Console . WriteLine (exc .Message ’’Файловая ошибка"); return ; } while(str "stop"); f str out.Close(); Использование класса StreamReader Для создания символьного потока ввода включите байтовый поток в класс Stream- Reader. Этот класс определяет несколько конструкторов. Наиболее часто использу- ется следующий: StreamReader(Stream stream) Здесь параметр stream определяет имя открытого потока. Если значение параметра stream равно нулю, генерируется исключение ArgumentNullException. Сразу же после создания класс StreamReader будет автоматически выполнять преобразование' байтов в символы. Следующая программа представляет собой простую утилиту; она считывает тексто- вый файл test .txt и отображает его содержимое на экране. Таким образом, данная утилита является комплементарной по отношению к утилите записи на диск, при- веденной в предыдущем разделе. /* Простая утилита, которая демонстрирует возможности класса FrleReadcr. */ using System; using System.10; class DtoS { catch(lOException exc) | Console-WriteLine(exc.Message - ” return ; итыванис строк из файла с последующим ? отображением на экране. j F’l i eHode . Open ) ; : 1 1 1 Невозможно открыть файл.’’); । л j StreamReader f strain - new StreamReader(fin); 1 while ( (s - fstrain.ReadLine()) - null) { Console.WriteLine (s); | ) I f strain.Close () ; 1
Перенаправление стандартных потоков 40! гюрагите внимание на то. каким образом определяется конец файла. Как тольк ссылка, возвращаемая методом ReadLir.e (), получит значение, равное нулю, эт \... i/i:e i на достижение конца файла. К. к и в случае с классом streamwriter, в некоторых случаях можно открыть фай и,посредственно. воспользовавшись возможностями класса StreumReader, Для вы • н, ч1спия этой операции служит следующий конструктор: ^atiReader (string filename} >,.cei> параметр filename указывает имя открываемого файла, причем может задаватьс не птьгй путь. Существование файла является обязательным. Если файл не существуе' 1С1ерируется исключение loExcepticr. Если параметр filename представляет собо п\лую строку, генерируется исключение ArguraentExceptior. Минутный практикум 1. Какой класс используется для считывания символов из файла? 2. Какой класс применяется для записи символов в файл? 3. Зачем нужны отдельные символьные классы ввода/вывода? Перенаправление стандартных потоков Как упоминалось ранее, стандартные потоки, такие как Console. In, могут быт перенаправлены. Конечно, наиболее часто потоки перенаправляются в файл. Ка юлько стандартный поток перенаправлен, вводимые и выводимые данные переш правляются автоматически. Путем перенаправления стандартных потоков програмх- можст считывать команды из файла на диске, создавать журнальные файлы либо да» во нчать данные из сети. Ответы профессионала и? rm Вопрос. Я слышал, что можно указать «символьная кодировка» путем открь 'ИЯ класса StreanFeader или Strear.Writrг. В чем суп, символьного кодирован! " как его можно использовать? Ответ. Классы StrearcReader и Streamwriter преобразуют байты в символ " наоборот. При этом используется символьная кодировка, указывающая спосс Преобразования. По умолчанию в C# используется кодировка UTF-8, котор; является совместимой с кодировкой Unicode. Поскольку ASCH является подмнож< сивом Unicode, свойство кодировки, заданное по умолчанию, обрабатывает символ II и Unicode. Для указания другой кодировки можно воспользоваться перегр' жечнымн версиями конструкторов StreamReader пли StrearrWriter, которь 1!|'-'|очаю1 параметр кодирования. Иначе говоря, символьная кодировка трсбусп ^’вольно редко. Для считывания символов используется класс screamReaaer. 2 - Для записи символов применяется класс Streamwriter. 3 Символьные классы ввода/вывода автоматизируют прямое и обратное преобразован oyte-char.
406 Глава 11. Ввод/вы| Перенаправление стандартных потоков выполняется двумя путями. Во-первых, при' вызове программы в командной строке можно воспользоваться символами > и < выполняющими перенаправление в Console, in и в Console.Out, соответственно j Например, обратите внимание на такую программу: | using System; class Test ( public static void MainO { Console.WriteLine("Это текст. " ) ; j ) При вызове программы следующим образом: Test > log строка "Это текст . " записывается в файл log. Ввод может перенаправляться таким же образом. Единственное, что нужно помнить при направлении ввода, — убедиться в том, что источник входной информации передает количество входных данных, достаточное для удовлетворения требований программы. Если же это требование не: выполняется, программа «зависает». Операторы перенаправления командной строки (< и >) не являются частью С#, но они поддерживаются операционной системой. Таким образом, если в среде поддер- живается перенаправление операций ввода/вывода (как в случае с Windows), можно перенаправлять стандартный ввод и вывод без выполнения каких-либо изменений в программе. Однако существует и второй способ перенаправления стандартных пото- ков, при использовании которого применяется программный контроль. В этом случае необходимо воспользоваться методами setin (), SetOut () и SetError О , которые являются членами класса Console: static void Senin(TextReader input) static voio SetOut(Textwriter output) static void SetError(Textwriter output) Таким образом, для перенаправления вывода вызовите метод Setin (), указав тре- буемый поток. Можно использовать любой поток ввода в том случае, если он наследуется из класса TestReader. Для перенаправления вывода в файл укажите файл, который входит в состав класса Textwriter. Следующая программа демонст- рирует соответствующий пример. // Перенаправление Console.Out. using System; using System.IO; class Redirect { public static void Main() { Streamwriter log__out; try { log__out - new Streamwriter (’’logfile. txt”) ; ) catch(lOException exc) {
Перенаправление стандартных потоков 407 Console .WriteLine (exc .Message + "Невозможно открыть файл."); return ; ' 'i.nso1 е SetOut (log out) ; ◄_____| Перенаправление потока Ccnso 1 e.Out. Console.WriteLine("Начало журнального файла.”); for (int i-0; K1C; i-rt) Console .WriteLine (i) ; Console.WriteLine("Конец журнального файла.”); _og out.Close(); После запуска программы на выполнение на экране ничею не отображается. Весь вывод направляется в файл logfile.txt: Начало журнального файла. С 1 2 3 4 5 ь 9 Кснец журнального файла. Теперь вы можете поэкспериментировать с другими встроенными потоками. Проект 11-1. Утилита сравнения файлов 0 CompFiles.cs В лом проекте разрабатывается простая, но весьма полезная утилита, используемая при сравнении файлов. Эта утилита открывает оба сравниваемых файла, а затем считывает и сравнивает каждый соответствующий набор байтов. Если при этом обнаруживается несоответствие, файлы считаются различными. Если одновременно Достигнуты концы обоих файлов и несоответствие не найдено, файлы считаются одинаковыми. Пошаговая инструкция I- Создайте файл CompFiles . cs. - В файл ConpFiles.cs введите следующий код: Проект 11-1. Сравнение двух файлов. Укажите имена двух сравниваемых файлов в командной строке. Например: CompB'ile FIRST.TXT SECOND.TXT
408 Глава 11. Ввод/вывод; using System; using Syslei . Ю; class Comprises t public static void Main(string;i args) int i-O, j 0; Fi leSt Li?ar" f-2; // Открытие пера о io файла, try i fl - new File-Stream (args [0] , FileMode.Open) ; ; catch(FileNotFoundException exc) { Console.WriteLine(exc.Message); return; } > • Открытие второю файла. f2 - new FileStrear(args.1Jt FileMode.Open}; ► catch(FilcNctFoundExceptIon exc) { Console.WriteLine(exc.Message); return; } catch(IndexOutOfRangeExcepcion exc) t Console.WriteLine(exc.Message + ’’XnUsage: CoirpFile fl f2"); return; } // Сравнение файлов, try { i f1.ReadByte(); ] f2.ReadByte(); if(i ! j) break; । while(i !- -1 && j 1- 1); catch(lOException exc) { Console . Wri t eLir.e (exc . Message) ; Console . Wri teLine (’’Файны различны.") ; С о n s о 2 e . Wr i t e L i. ne (” Фай пн co в u.a даЕ?ч-. " ) ; fl .Close(); fZ.Clrsel) ; 3. С целью тестирования программы JmvF.’.es скопируйте файл CoispFiles.cs в файл tcrip. Затем выполните следующую команду: CompFi cs Cor.pF: Les . са te:-p Нршрамма должна сообщи п, о юм, чю файлы совпадают. i
чтение и запись двоичных данных 409 4 Затем сравните файлы CnopFios . с:-. и •’:< yySi : • юваншись следующей командой: (пиками рапсе), bociio.ii>- ?mpF lies CompFiles.es CopyFile.es Программу Comp-Files можно обогатить различными опциями. Например, мож- но добавить опцию, позволяющую программе игнорировать регистр символов. Другое расширение ее возможностей заключается в том. что программа ' >ту- ' lies могла бы отображать позиции отличающихся данных в файлах. Чтение и запись двоичных данных До сих пор мы рассматривали механизмы чтения и записи баигов и символов, но следует учитывать, что можно производить >.тсние и запись данных других типов, например значений типа int, coutle или snorr. Для чтения и записи двоичных значений, имеющих встроенные типы данных С#, применяются потоки Вгг.асу- р.с зег и Binary-Writer. При использовании этих потоков важно понимать, что данные считываются и записываются во внутреннем двоичном формате, а не в текстовой форме, воспринимаемой человеком. Поток BinaryWriter Поток Binarywriter представляет собой оболочку для байтового потока, которая управляет записью двоичных данных. Ниже показан наиболее часто используемый конструктор: E.-.-aryWriter (Stream outputstream) Здесь параметр outputstream определяет поток, в который производится запись Данных. Для записи вывода в файл в качестве этого параметра указывается объект, созданный конструктором Filestream. Если значение параметра outputstream равно нулю, генерируется исключение ArgumentNuliE.iception. Если параметр с nctstream задаст поток, нс открытый для записи, генерируется исключение «г 'nimentException. П010К SinaryWriter определяет методы, которые обеспечиваю! запись данных всех вкроенных типов С#. Многие из этих методов приведены в шбл. 11.5. IIoiok iryWriter также определяет стандартные методы ric-.se о и Fiu.snO. pa6oia К(!горых описывалась ранее. Таблица 11.5. Наиболее часто используемые методы, определенные потоком BinaryWriter Метод J Write (soyte val} • d Write (tyre val} v . a write (Byte[J but} id write (short val} id Write (ushort val} id Write (int val} 'mid Write (uint val) Описание Запись байта co знаком Запись байта без знака Запись массива байтов Запись короткого целого Запись короткого целого без знака Запись целого числа Запись целого без знака
410 Глава 11. Ввод/выв( Метод Описание vc i d Write (long val) Запись длинного целого void Write (ulong val) Запись длинного целого без знака voi d Write (float val) Запись значения типа float void Write (double val) Запись значения типа dour? Le void Write (char val) Запись символа void Write (chart] val) Запись массива символов void Write (string val) Запись строки Поток BinaryReader ж Поток Binary-Reader является оболочкой, которая включает байтовый поток, вы-S. иолняюший чтение двоичных данных. Ниже показан наиболее часто используемыйЖ' конструктор для этого потока. 1 BinaryReader(Stream inputstream) ' Здесь параметр inputstream обозначает поток, из которого происходит считываний' данных. Для выполнения считывания данных из файла в качестве этого парамет можно указать объект, созданный с помощью конструктора FileStream. Ес значение параметра inputstream равен нулю, генерируется исключение Argument.-, KullException. Если параметр inputstream не открыт для чтения, генерирует^ исключение ArgumentException. Поток BinaryReader поддерживает методы, обеспечивающие считывание всех проЗ стых типов данных в С#. Наиболее часто используемые из них приведены в табл. И Поток BinaryReader ниже. также определяет три версии метода Reado, приведенные int Read() Read(byte[] buf, int offset, int num) Возвращает целочисленное представ-1. А ление следующего доступного символа? £ из потока ввода. При достижении конца, -'j файла возвращает значение -1 Осуществляет попытку считать лит бай- 1; тов в буфер buf, начиная с элемента,. < находящегося в позиции offset, возвра-’ щая при этом количество успешно счи- танных байтов Read(chart) buf, int offset, int num) Осуществляет попытку считать пит сим- волов в буфер buf, начиная с элемента, находящегося в позиции offset, возвра- щая при этом количество успешно счи- танных символов В результате сбоя при выполнении этих методов генерируется исключение сер' .. Также определен стандартный метод close О.
Чтение и запись двоичных данных 411 Таблица 11.6. Наиболее часто используемые методы, определенные потоком BinaryReader Метод _____________________Описание ГУ' ' ReadBoo Lean () Чтение значения типа bool Ь’х ce ReadByte() Чтение значения типа byte ,re ReadSByte() Чтение значения типа sbyte о у :e|J ReadBytes(int num) Чтение пит байтов и возврат их в виде массива Ct jr ReadChar () Чтение значения типа char ch л[J ReadChars(int, num} Чтение пит символов и возврат их в виде массива de uble ReadDoubleO Чтение значения типа double 4= .at ReadSingle() Чтение значения типа float s'-. ait Readlntl6() Чтение значения типа short 11. t Readlnt32() Чтение значения типа int 1c ng Readlnt64() Чтение значения типа long US r.ort ReadUintl6() Чтение значения типа ushort UJ nt ReadUint32() Чтение значения типа uint ul ong ReadUint64() Чтение значения типа ulong st ring Readstring() Чтение строки Демонстрация двоичного ввода/вывода Ниже приводится пример программы, которая демонстрирует возможности потоков BinaryReader и BinaryWriter. Эта программа сначала записывает в файл, а затем считывает из него значения различных типов. /* Закись и считывание двоичных данных, using System; using System.IO; class RWData { public static void Main() { BinaryWriter dataOut; BinaryReader detain; int i - 10; double d = 1023.56; tool b = true; try { dataOut = new BinaryWriter(new Filestream("testdata"r FileMode.Create) ) ; catch(lOException exc) { Console . WriteLine (exc .Message +• " ХпНевозможно открыть файл.’’); return; try { Console.WriteLine("Запись " ы i);
412 Глава 11. Ввод/выеод dataOut.Write(1); — - —— Console.WriteLine("Запись " + о); dataOuu. Wr ite (d) ; -ч— {Запись двоичных данных. 1 Console.WriteLine("Запись " a b); Console.WriteLine("Запись " - 12.2 x 7.4); dataOut.Write(12.2 * 7.4) ; 4— catch(ICException exc) ( Console. WriteLine (exc . Message - '"хпОшибка записи.*'); } dataOut.Close(); Console.WriteLine() ; // Теперь снова чтение. cry { dataln = new BinaryReader(new FileStream("testdata", FileMode.Open)); } catch(lOException exc) { Console.WriteLine(exc.Message + ”\nHeвозможно открыть файл."); return; } try { i -= dataln.Readlnt32(); -<-------—— — --•-----———------------— Console.WriteLine ("Чтение " t- i) ; d -= dataln . ReadDouble ();-«- -~----------------—n Console.WriteLine ("Чтение " т d) ; r~——'------— ------- Чтение двоичных данных. b = dataln . ReadBoolean (); ^<---—---—----------—J Console.WriteLine ("Чтение ” -r b) ; d - dataln.ReadDouble();4--------------------------— Console.WriteLine("Чтение ’’ 0) ; } catch(lOException exc) { Console .WriteLine (exc . Message * "Оыибка чтения."); dataln.Close(); } } Результат выполнения программы: Запись 10 Запись 1023.56 Запись True’
произвольный доступ к содержимому файла 9С.28 Минутный практикум i ЭИ. Какой поток используется при записи в двоичные файлы? e\J 1. Какой метод вызывается для записи значения типа double в дв формате? 3. Какой метод вызывается для чтения значения гипа short в дв< формате? Произвольный доступ к содержимому файл До настоящего момента времени нами использовался гак называемый последы ныи доступ к содержимому файла, байт за байтом. Однако в C# возмс произвольный доступ к содержимому файлов. Для реализации подобного i используется метод Seek (), определенный в классе FileStreani. Этот метод ляс! задавать индикатор позиции текущего элемента данных в файле (которые называется файловый указатель). Синтаксис метода Seeri ) таков: Ic.q Seek (long newPos, SeekOrigin origin) Здесь параметр newPc s указывает на новую позицию (в байтах) для файлового ука начиная от местоположения, указанного параметром ед.дгг. Последний па может иметь одно из значений, определенных в перечислении сеекОпдк. Значение Описание : ent Отсчет от начала файла Отсчет от текущего местоположенин Отсчет от конца файла После вызова метода Seek () следующие операции чтения либо записи произвг 8 новой позиции файлового указателя. Если во время позиционирования ука произойдет ошибка, генерируется исключение iGException. Если поток не п< «иваег операцию позиционирования, генерируется исключение HotSi.prort И-гл-с приводится пример, демонстрирующий способ выполнения опсрациз вывода с произвольным доступом к содержимому файла. В этом примере и Похищаются прописные буквы ал фавн га. когорыс затем счи г ываются в произво. Порядке. । При записи в двоичные файлы используется поток BinaryWr-ter. Для записи значения типа сюшже вызывается метод Wrcce () . 3- Для чтения значения типа shore вызывается метод гежДж 16 () .
г 414 Глава 11. Ввод/вывод^ // Демонстрация механизма произвольного доступа к содержимому файда, иsang System; using System.IO; class RandomAccessDemo { public static void Main() { FileSbream f; char ch; try { f - new FileStream("random.dat", FGeMode. Create) ; 1 catch(FileNotFounaException exc) { Console.WriteLine(exc.Message); return ; // Запись букв в алфавитном порядке. for(int i-0; i < 26; i++) { try { f.WriteByte((byte)(*Ar*i)); } catch(lOException exc) { Console.WriteLine(exc.Message); return ; } } try { // Считывание букв. f.Seek(0, SeekOrigin.Begin); // Поиск ch - (char) f.ReadByte(); Console.WriteLine("Первое значение ” + Использование метода Seek() для! перемещения файлового указателя?] первого байта . «<--- ? ch) ; ' f.Seek(l, SeekOrigin.Begin); J/ Поиск второго байта. •<- ch (char) f.ReadByte(); Console.WriteLine("Второе значение " -r ch); f.Seek (4, SeekOrigin.Begin); // Поиск пятого байта, ch (char) f.ReadByte(); Console.WriteLine(Пятое значение " + ch); Console.WriteLine(); // Теперь чтение четных букв. Console.WriteLine("Четные буквы: "); for(int i-0; i < 26; i 2) { f.Seek(i, SeekOrigin.Begin); // Поиск i-го байта ch (char) f.ReadByte(); Consdle.Write(ch 4 " "); } } catch(lOException exc) { Console.WriteLine(exc.Message); } f.Close();
Преобразование числовых строк в их внутренние представления 415 результат выполнения программы: первое значение А Ф,?оое значение В Ч^-.ое значение Е З^.е-си каждое другое значение: /д Е G I К М О Q S U W Y ® Минутный практикум I. Каким образом позиционируется файловый указатель? 1 ' \ у 2. Какое значение параметра origin задает отсчет от текущего значения файлового указателя? 3. Какое исключение генерируется, если метод Seek () вызывается для потока, который не поддерживает позиционирование? Преобразование числовых строк в их внутренние представления Перед завершением рассмотрения механизмов ввода/вывода обратимся к технике, которая используется при чтении числовых строк. Как вы помните, метод Write- Lire () в C# обеспечивает удобный способ вывода данных различных типов на консоль. Сюда также включаются числовые значения встроенных типов данных, таких как int и double. В результате метод WriteLine () автоматически преобразует числовые значения в их представления, более удобные для восприятия человеком. Однако в СД нс поддерживается метод ввода, который мог бы считывать и преобра- зовывать строки, содержащие числовые значения, в их внутреннее двоичное пред- ставление. Например, невозможно ввести с клавиатуры такую строку, как «100» и автоматически преобразовать ее в соответствующее целочисленное значение, которое может быть сохранено в переменной типа int. Для выполнения этой задачи необхо- димо воспользоваться методом, который определен для всех встроенных числовых типов: Parse () . Перед тем как перейти к дальнейшему рассмотрению вопроса, необходимо вспомнить один важный факт: все встроенные типы С#, такие как int и double, на самом деле являются псевдонимами (другими именами) для структур, определенных в библио- теке .NET Framework. Фактически Microsoft утверждает, что тип языка C# и тип С11’\кгуры .NET практически совпадают. Просто используются различные наимено- вания. Поскольку типы значений C# поддерживаются структурами, то эти типы являются членами соответствующих структур. Ниже показаны типы числовых значений C# и соответствующие им имена структур М Г. И Для позиционирования файлового указателя используется метод Seek (). значение SeekOrigin . Current. 3- Если метод Seek () не поддерживается потоком, генерируется исключение NotSupported- Exception.
416 Глава 11. Ввод/вьп Имя структуры .NET Тип данных C# Double double gK Single Intl6 float short »I. Int32 Int -ж I n 16 4 long UIntl6 ushort Л Ulnt32 uint "> Ulnt64 ulong w Byte Sbyte byte Ж sbyte ж Метод для преобразования static double Parsefstring str) static float Parsefstring str) static long Parse(string str) static int Parsefstring str) static short Parsefstring str) static ulong Parsefstring str) static uint Parse(string str) static ushort Parse(string str) static byte Parsefstring str) static sbyte Parse(string str) Эти структуры определены в пространстве имен System. Таким образом, полностью определенное имя структуры Int32 будет System. Int32. Эти структуры предлагают Д' широкий набор методов, позволяющих полностью интегрировать типы значений в Ж иерархию объектов С#. Также числовые структуры определяют статические методы, Яр преобразующие числовую строку в соответствующий ей двоичный эквивалент. Эти?» методы приведены ниже. Каждый из них возвращает двоичное шачсние, еоответстм® вуюшес строке. Структура Double Single. Int 64 I n 13 2 Intl6 UInt64 UInt32 UIntl6 Byte SByte Методы Parse () генерируют исключение ForroatException, если параметр str не ’ содержит корректного значения, соответствующего нужному тину данных. Исклю- ; чснис ArgumentNullException генерируется, если параметр str имеет значение: nul 1, а если строка str после преобразования нс может быть помещена в переменную . соответствующего типа, генерируется исключение Cverfic^Except юл. Методы Parse () обеспечивают удобный способ преобразования в числовое значение, строки, считываемой с клавиатуры или из текстового файла, в подходящий внутрен- ний формат. Например, в следующей программе подсчитывается среднее значение чисел, введенных пользователем. Сначала программа просит указать количество значений, для которых рассчитывается среднее значение. Затем она считывает это число с помощью метода ReadLine и посредством метода int32 . Рагге о прсобра-Ж зовываст строку в целочисленное значение. Далее осуществляется ввод значений, ж причем метод Double. Parse () используется для преобразования строк в их число-'в вые эквиваленты, имеющие тип double.
преобразование числовых строк в их внутренние представления /* Э'га программа подсчитывает среднее значение введенных чисел using System; using System.IO; class AvgNums { public static void Main() ( string str; :nt n; aouble sum - 0.0; rouble avg, t; Console.Write("Укажите количество вводимых чисел: . tr Console.ReadLine(); try ( ______________________________ n “ Int32.Parse(str); -4--------j Преобразование строки в число типа int. catch(FormatException exc) { Console.WriteLine(exc.Message); n - 0 ; catch(OverflowException exc) ( Console.WriteLine(exc.Message); n - 0; Console.WriteLine("Введите " + n + " значений."); for(int i=0; i < n ; i++) { Console..Write (": "); str = Console.ReadLine(); try { ----------------------------------- t - Double. Parse (str) ;<___Преобразование строки в число типа double. ) catch(FormatException exc) { Console.WriteLine(exc.Message); t = 0.0; catch(OvertlowException exc) { Console.WriteLine(exc.Message); t » 0; } sum += t; avg = sum / n; Console.WriteLine("Среднее значение " + avg) ; Результат выполнения программы: Укажите количество вводимых чисел: 5 5 значений. : 2.2 : !.3 : 4.4 5 Среднее значение 3.3
418 Глава 11. Ввод/ei С помощью методов Parse() можно улучшить калькулятор платежей по ссу рассматриваемый в проекте 2-3. В старой версии величина ссуды, значения проце, ной ставки, а также некоторые другие значения жестко задавались в самой программ Программа станет более универсальной, если эти значения будут определят, пользователем. Ниже приводится код усовершенствованного калькулятора платеж* по ссуде. Улучшенный проект 2-3 Вычисление регулярных платежей по ссуде. using System; class RegPay { public static void Main() { decimal Principal; // Величина ссуды decimal IntRate; // Процентная ставка в виде десятичной дроби // (например, 0.075) decimal PayPerYear; // Количество платежей в год decimal NumYears; // Количество лет decimal Payment; // Размер платежа decimal numer, denom; // Временные переменные double b, е; // Параметры вызова метода Pow() string str; Console.Write("Введите величину ссуды: "); str = Console.ReadLineО; try { Principal - Decimal.Parse(str); ) catch(FormatException exc) { Console.WriteLine(exc.Message); return; } Console.Write("Введите процентную ставку (в виде 0.085): "); str = Console.ReadLine(); try ( IntRate - Decimal.Parse(str); } catch(FormatException exc) ( Console.WriteLine(exc.Message); return; Console.Write("Введите количество лет: "); str = Console.ReadLine(); try { NumYears = Decimal.Parse(str); } catch(FormatException exc) { Console.WriteLine(exc.Message); return; } Console. Write ("Введите количество платежей в год: ");
рреобразоеание числовых строк в их внутренние представления 419 str = Console.ReadLine(); try { PayPerYear - Decimal.Parse(str) ; i catch(FormatException exc) { Console.WriteLine(exc.Message); return; numer = IntRate * Principal / PayPerYear; e - (double) -(PayPerYear * NumYears); c (double) (IntRate / PayPerYear) ч 1; cienom - 1 - (decimal) Math.Pow(o, e) ; Payment numer / denom; Console.WriteLine("Платеж {0:C}", Payment); } } Результат выполнения программы: Взедиге величину ссуды: 10000 Введите процентную ставку (в виде 0.085): 0.075 Введите количество лет: б Введите количество платежей в год: 12 Платеж $200.38 Ответы профессионала .........-..-.-...-->... Вопрос. Для чего еще пригодны структуры значение-тип, такие как Int32 или Double? Ответ. Структуры значение-тип поддерживают несколько методов, облегчаю- щих интеграцию встроенных типов C# в иерархию объектов. Например, все струк- туры обладают методами CompareTo (), которые используются для сравнения значе- нии, методами Equals (), которые проверяют два значения на равенство; а также методами, которые возвращают значение объекта в различных формах. Числовые структуры также включают поля MinValue и Maxvalue, содержащие максимальное и минимальное значения, которые могут быть сохранены посредством объекта Данного типа. Проект 11-2. Создание справочной системы Ej PiieHelp.cs В проекте 4.1 был создан класс Help, выдающий сведения об управляющих опера- торах С#. В подобной реализации справочная информация хранится в классе, а пользователь может выбирать ее в меню, воспользовавшись нумерованными опция- "II. Хотя этот подход является вполне функциональным, вообще говоря, он не идеален при создании справочной системы. Например, если понадобится изменить справочную информацию, придется вносить изменения в исходный код программы. Кроме того, выбор тем справки по номерам является более утомительным, чем поиск
420 Глава 11. Ввод/выво) по имени, и неприемлем в случае обширных списков тем. Поэтому этот недостаток был устранен в следующей версии справочной системы. а Справочная система хранит информацию в файле справки. Этот файл представляем собой стандартный текстовый файл, который можно изменять и расширять щу желанию пользователя, не прибегая при этом к изменению самого кода программы,. Пользователь может получать нужную ему информацию путем ввода имени желаемой' темы. После этого справочная система производит поиск согласно введенному ключевому слову. Если соответствующая информация найдена, она отображается на- экране. * Пошаговая инструкция I. Сначала потребуется создать файл, который будет использоваться справочной! системой. Этот файл является стандартным текстовым файлом, который може1 быть организован следующим образом: #имя-темы1 справка по теме 1 #имя-темы2 справка по теме 2 #имя-темыЫ справка по теме N Имя каждой темы должно находиться в отдельной строке и предваряться сим-i волом #. Благодаря использованию знака # программа может быстро находит^, начало каждой темы. После имени темы может указываться произвольное коли-' чсство строк справочной информации, посвященной этой теме. При этом нужно позаботиться о пустой строке между последним предложением справки по одной теме и началом следующей. После конца каждой строки не требуются завершаю- щие пробелы. Ниже приводится простой пример справочного файла, который может использо- ваться для тестирования справочной системы. Этот файл хранит сведения об управляющих операторах С#. iuf if(condition) statement:; else statement; #switch switch(expression) { case constant: statement sequence break; // ... } #for
[Преобразование числовых строк в их внутренние представления 421 for(init; condition; iteration) statement; awhile while(condition) statement; frdo io { statement; while (condition); wreak creak; or break label; ^continue continue; or continue label; Назовите этот файл helpfile . txt. 2. Создайте файл FileHelp.es. 3. Начните создавать новый класс Help, воспользовавшись следующими строками кода: class Help { string helpflle; // Имя справочного файла. public Help(string fname) { helpfile = fname; } Имя справочного файла передается конструктору Help и хранится в экземпляре переменной helpfile. Поскольку каждый экземпляр класса Help может содер- жать свою собственную копию файла helpfile, каждый экземпляр может пре- доставлять справку по отдельной теме. 4- Добавьте метод helpon (), код которого приведен ниже, в класс Help. Этот метод используется для выборки справки по указанной теме. ' Отображение справки по теме. public bool helpon(string what) { StreamReader helpRdr; int ch; string topic, info; try { helpRdr = new StreamReader(helpfile); } catch(FileNotFoundException exc) { Console.WriteLine(exc.Message); return false; } try { do { // Считывание символов, пока не будет найден символ # ch = helpRdr.Read(); // Проверка найденной темы на соответствие искомой if(ch =- ’ # ') { topic — helpRdr.ReadLine();
422 Глава 11. Ввод/вь^ц| j if (what =- topic) { // Найдены темы, do { info - helpRdr.ReadLine(); if(info I- null) Console.WriteLine(info); } while ((info !- null) && (info *'”)); helpRdr.Close(); return true; } } } while(ch !- -1); } catch(lOException exc) { Console -WriteLine(exc.Message); } helpRdr.Close(); return false; // Тема не найдена } Класс Help открывается с помощью метода StreamReader. Поскольку справоч- ный файл содержит текст, за счет использования символьного потока справочная система обеспечиват поддержку различных языков. Метод helponf) работает следующим образом. Строка, содержащая имя темы, передается в параметре what. Затем открывается справочный файл. После этого метод производит поиск в файле, сравнивая значение параметра what и наиме- нование темы, находящейся в файле. Помните, что каждая тема начинается знаком #, поэтому цикл поиска просматривает файл на предмет наличия симво- лов #. Как только очередная тема будет найдена, производится проверка на предмет того, соответствует ли тема, обозначенная знаком #, теме, соответствую- щей ключевому слову what. Если соответствие установлено, отображается инфор- мация, связанная с этой темой. В случае обнаружения соответствия метод help- on () возвращает значение true. Если же соответствие не установлено, он возвращает значение false. 5. Класс Help также поддерживает метод getSelection (): // Получение справки по указанной теме, public string getSelection() { string topic = Console .Write (’’Введите тему: "); try { topic ~ Console.ReadLine(); i catch(lOException exc) { Console.WriteLine(exc.Message); return ”” ; } return topic; } 6. Ниже приводится полный код справочной системы. /* Проект 11-2 Справочная система использует файл на диске для хранения текстов справки.
Преобразование числовых строк в их внутренние представления 423 using System; using System.10; /* Класс Help открывает справочный файл, ищет тему, затем отображает информацию по этой теме. */ class Help { string helpfile; // Имя справочного файла. public Help (string fnarne) { helpfile = fnarne; } // Отображение справки по теме. public bool helpon(string what) { StreamReader helpRdr; int ch; string topic, info; try { helpRdr = new StreamReader(helpfile); } catch(FileNotFoundException exc) { Console.WriteLine(exc.Message); return false; } try { do { // Считывание символов, пока не будет найден символ # ch -= helpRdr.Read(); // Сравнение тем. if(ch == ’#’) { topic - helpRdr.ReadLine(); if(what == topic) { // Тема найдена do { info - helpRdr.ReadLine() ; if(info null) Console.WriteLine(info); } while((info !- null) && (info !- ”")); helpRdr.Close() ; return true; } } } while(ch ’= -1); } catch(lOException exc) { Console.WriteLine(exc.Message); } helpRdr.Close() ; return false; // Тема не найдена. } // Получение справки по указанной пользователем теме. public string getSelection() { string topic Console.Write (’’Введите тему: ”); try { topic = console. ReadLine () ,-
424 Глава 11. Ввод/bhi catch(lOException exc) { Console.WriteLine(exc.Message); return return topic; } } // Демонстрация возможностей файловой справочной системы, class FileHelp { public static void MainO { Help hlpobj - new Help ("helpf lie. txt*’) ; string topic; Console-WriteLine (’’Проверка справочной системы. " -г "Введите stop для выхода."); do { topic = hlpobj.getSelectionО; if(Ihlpobj.helpon(topic)) Console.WriteLine("Тема не найдена.\n"); } while(topic ! = "stop"); Вопрос. Ра профессионала —............-..- iee в этой главе уже упоминался класс потоков MemoryStreai^ используемый для организации хранилища в памяти компьютера. Каким образоЦ может использоваться этот класс? i, Ответ. Класс Memorystream является реализацией класса Stream, которая использует байтовый массив для организации ввода/вывода. Ниже приводится вызов одного из конструкторов этого класса. ' MemorySytean; (by te [ ] buf} Здесь параметр buf представляет собой байтовый массив, используемый в качестве источника или цели для запросов ввода/вывода. Поток, создаваемый с помощью конструктора, можно записывать или считывать. Он поддерживает метод SeekO- Необходимо помнить о том, что значение параметра but должно быть достаточно большим, чтобы класс Memorystream мог принять весь адресуемый ему объем данных. Потоки, использующие хранилища в памяти, достаточно широко используются в программировании. Например, можно организовывать поэтапный комплексный вывод, сохраняя промежуточные фрагменты информации в массиве. Эта техника является особенно полезной! при программировании в графической среде, такой как Windows. Можно также перенаправлять стандартный поток для чтения информации из массива. Например, это может оказаться полезным при тестировании программы. И последнее замечание. Для создания символьного потока, основанного на исполь- зовании памяти, воспользуйтесь потоками StringReader или StrrngWriter.
425 контрольные вопросы f Контрольные вопросы 1. Почему в C# определяются байтовые и символьные потоки? 2. Какой класс находится на вершине иерархии потоков? 3. Покажите, как следует открывать файл для считывания байтов? 4. Покажите, как следует открывать файл для считывания символов? 5. Для чего служит метод Seek () ? 6. Какие классы поддерживают двоичный ввод/вывод для встроенных типов данных? 7. Какие методы используются для перенаправления стандартных потоков под управлением программы? 8. Каким образом можно преобразовать числовую строку, такую как «123.23», в ее двоичный эквивалент? 9. Создайте программу, копирующую текстовый файл. В результате вы- полнения программы все пробелы должны заменяться символами дефи- сов. Используйте классы файлов байтового потока. 10. Перепишите программу из пункта 9 таким образом, чтобы она исполь- зовала классы символьных потоков.
глава Делегаты, события,| пространства имен | ^8 и дополнительные ф элементы языка C#f Ой ;Ь - ..— . 7|,< □ Делегаты □ События □ Пространства имен й □ Создание операторов преобразования □ Препроцессор • ••• □ Атрибуты -л □ Указатели и незащищенный контекст 1 □ Идентификация типа во время работы программы ;/ □ Дополнительные ключевые слова в C# ;ii
427 ' р(так. эта глава является последней в данной книге. В ней мы рассмотрим важные пемснты языка C# — делегаты, события и пространства имен, а также расскажем og операторах преобразования, атрибутах, директивах препроцессора и некоторых ерСдовых технологиях, которые применяются главным образом в особых ситуациях. делегаты Термин «делегат» звучит необычно для новичков, изучающих С#. На самом деле Джетты в понимании и использовании нс сложнее любых других элементов этого языка. По своей структуре делегат — это объект, который может ссылаться на метод. То есть при создании делегата создается объект, который может хранить ссылку на метол. Кроме того, эта ссылка может быть использована для вызова метода. Следо- вательно, делегат может вызывать метод, на который он ссылается. Хотя метод не является объектом, для него выделяется некоторая область памяти. Адрес этой области памяти — это точка входа метода, и именно адрес инициируется при вызове метода. Значение адреса метода (области памяти) может быть присвоено делегату. Если делегат ссылается на метод, то данный метод можно вызывать с помощью этого делегата. Кроме того, один и тот же делегат может использоваться при вызове различных методов, для этого ему присваивается ссылка на требуемый метод. Важным свойством делегата является то, что он позволяет указать в коде программы вызов метода, но фактически вызываемый метод определяется во время работы программы, а не во время компилирования. Замечание Если вы знакомы с языками C/C++, вам будет интересно узнать, что делегаты в C# сходны с указателями на функции в C/C++. Для объявления делегата необходимо использовать ключевое слово delegate. Общая форма синтаксиса объявления делегата показана ниже. delegate ret-type name (parameter-list) ; Здесь словосочетание ret-type — тип значения, возвращаемого методами, которые будут вызваны делегатом. Слово name — имя делегата. Параметры, необходимые Методам при их вызове с помощью делегата, указываются в списке параметров Ра. -.meter-list. Делегат может вызывать только те методы, типы возвращаемых значений и списки параметров которых совпадают с типами и списками, указанными при объявлении делегата. Щ уже говорилось, характерной особенностью делегата является возможность его ^пользования для вызова любого метода, который соответствует подписи делегата. Это •Нас г возможность определить во время выполнения программы, какой из методов должен °Ыть вызван. Вызываемый метод может быть методом экземпляра, ассоциированным с объектом, либо статическим методом, ассоциированным с классом. Метод можно Вьввать только тогда, когда его подпись соответствует подписи делегата. р, •зесмотрим простую программу, в которой используется делегат. Простая программа, в которой демонстрируется использование делегата.
428 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка // Объявление делегата. ---_---------Л delegate string s trMod (string str) ; ч------[Делегат strMod. j | class DelegateTest { j // Метод заменяет пробелы дефисами в строке, передаваемой ему в качестве 1 // параметра. 3 static string replaceSpaces(string a) { ? Console . WriteLine ("Замена пробелов дефисами.’*); | return a.Replacef* ] } // Метод, предназначенный для удаления пробелов в передаваемой ему строке. 1 static string removeSpaces(string a) { J string temp - ; J int i; 1 Console-WriteLine (’’Удаление пробелов."); for(i=0; i < a.Length; i++) | if(a[i] ! = ' ') temp += a[i]; J return temp; C } j // Метод выводит передаваемую ему строку в обратном порядке. ‘ static string reverse(string a) { ; string temp - < int i, j; j Console.WriteLine("Вывод строки в обратном порядке."); for(j^0, i=a.Length-1; i >= 0; i—, j++) temp += a[i]; \ return temp; } public static void MainO { ff Создание делегата. strMod strop - new strMod (replaceSpaces) ; ----—[Создание делегата.j string str; // Вызов метода с помощью делегата. Вызов метода с использованием str - strOp("Проверка работы методов."); Ч—---------»—делегата. Console-WriteLine (’’Преобразованная строка: " + str) ; Console-WriteLine(); strop ~ new strMod(removeSpaces); str = strOp("Проверка работы методов."); Console-WriteLine("Преобразованная строка: " + str); Console.WriteLine(); strOp =» new strMod(reverse); str strop ("Проверка работы методов."); Console. WriteLine ("Преобразованная строка: " + str);
делегаты______________________________________________________________________ результат выполнения программы: с имена пробелов дефисами. ^^образованная строка: Проверка-работы-методов. -L. ;ение пробелов . рr L образованная строка.: Проверкаработыметодов. р.-.^-гд строки в обратном порядке. р.-’образованная строка: . водотем ытобар акреворП рассмотрим эту программу более подробно. В ней объявляется делегат strMod, коюрый принимает один параметр типа string и возвращает значение типа spring. В классе DelegateTest объявляются три метода с модификатором static с соот- ветствующей делегату подписью. Эти методы предназначены для преобразования cipoKii, передаваемой им в качестве параметра. Обратите внимание, что метод . aceSpases () для замены пробелов дефисами использует один из методов класса ..r.g, который называется Replace О . В методе Main О создается объект strop типа strMod, которому присваивается ссылка на метод replaceSpaces О . В операторе s'.rMod strop - new strMod(replaceSpaces); метод replaceSpaces () передается конструктору делегата в качестве параметра. Используется только имя метода без указания параметра (то есть при создании экземпляра делегата указывается только имя метода, на который должен ссылаться делегат). Подпись метода должна соответствовать подписи, определенной в объявле- нии делегата. Если это условие не выполнено, при компилировании программы произойдет ошибка компиляции. В следующей строке кода экземпляр делегата strop используется для вызова метода .placeSpaces() . - strop("Проверка работы методов."); Поскольку объект strop ссылается на метод replaceSpases(), го вызывается именно этот метод. J.viee в программе экземпляру делегата strop присваивается ссылка на метод •noveSpaces (). после чего экземпляр делегата strop вызывается вновь. На этот раз инициируется метод removespaces (). В завершение программы экземпляру делегата strop присваивается ссылка на метод vers (), после чего вновь выполняется оператор '-г = strop ("Проверка работы методов."); На этот раз вызывается метод revers (). В данной программе при каждом вызове Щ земпляра делегата strop будет вызываться тот метод, на который ссылается делегат гор во время вызова. Таким образом, вызываемый метод определяется во время ыполнения программы, а не во время компилирования. В этом примере использовались методы с модификатором static, но делегатам может присваиваться также ссылка на методы экземпляра. Например, ниже приве- чена новая версия предыдущей программы, в которой операции со строкой инкап- сулированы внутри класса StringOps.
i, ииионин, npvuipcinui «ci ммен и диполнительные элементы языод] // Новая версия предыдущей программы, в которой делегаты ссылаются на // методы экземпляра. using System; [I Объявление делегата, delegate string strMod(string str); class StringOps { // Метод заменяет пробелы дефисами в строке, передаваемой ему в качестве // параметра. % public string replaceSpaces (string a) { Console.WriteLine("Замена пробелов дефисами."); ? return a.Replace(* } v ’ J // Метод, предназначенный для удаления пробелов в передаваемой ему строке.f public string removespaces(string a) { String temp = 1 ’ int i; Console.WriteLine("Удаление пробелов."); • X for(i=0; i < a.Length; i++) •,;* if(a[i] ’ ') temp +- a[i]; j return temp; j } J // Метод выводит передаваемую ему строку в обратном порядке. public string reverse(string a) { < string temp = j int i, j; f Console.WriteLine("Вывод строки в обратном порядке."); for(j=0, i=a.Length-1; i >= 0; i —, j temp 4— a [ i} ; return temp; class DelegateTest ) public static void MainO ( StringOps so = new StringOps (); // Создание экземпляра делегата. strMod strop = new strMod (so. replaceSpaces); +- string str; Создание делегата e использо- ванием метода экземпляра. // Вызов метода с помощью делегата. str = strop("Проверка работы методов.”); Console.WriteLine("Преобразованная строка: ” + str); Console.WriteLine(); strop = new strMod(so.removespaces); str ~ strOp("Проверка работы методов.”);
Console.WriteLine("Преобразованная строка: ” + str); Console.WriteLine(); atirOp = new strMod (so. reverse) ; str - strop("Проверка работы методов."); Console-WriteLine("Преобразованная строка: " + str); } i 3 результате работы этой программы на экран выводятся тс же строки, что и в предыдущей. Но в данном случае делегат ссылается на методы, используя экземпляр класса stringops. Многоадресность делегатов Одним из замечательных свойств делегата является его способность хранить несколь- ко адресов области памяти. Последовательно инициируя эти адреса, делегат может один за другим вызывать соответствующие методы. Эта характеристика делегатов называется многоадресностыо. Такая последовательность вызываемых методов, или цепочка методов, конструируется достаточно просто — сначала необходимо создать экземпляр делегата, а затем использовать оператор += для добавления методов к цепочке. Хотя фактически при этом создается не цепочка методов, а цепочка ссылок на истоды, в результате чего этот делегат становится многоадресным. Для удаления метода из цепочки используется оператор -=. (Конечно, для добавления и удаления методов из цепочки можно по отдельности использовать операторы +, - и =, но применение составных операторов присваивания += и -= более удобно.) Единствен- ное ограничение — делегаты, хранящие несколько ссылок, должны иметь тип воз- вращаемого значения void. Ниже приведена программа, демонстрирующая использование многоадресного деле- гата. Это новая версия предыдущей программы, в которой в качестве типа возвра- щаемого значения методов, преобразующих строку, используется тип void, а для возвращения вызывающей подпрограмме измененной строки применяется модифи- катор параметра ref. Л В прох’рамме демонстрируется использование многоадресное™ делегата. using System; ' Объявление делегата. delegate void strMod(ref string str); Lass StringOps { '/ Метод заменяет пробелы дефисами в строке, передаваемой ему в качестве ’/ параметра. static void replaceSpaces(ref string a) { Console-WriteLine("Замена пробелов дефисами."); a - a.Replace(' / Метод, предназначенный для удаления пробелов в передаваемой ему строке, static void removespaces(ref string a) { string temp = int i; Console.WriteLine("Удаление пробелов."); for(i=0; i < a.Length; i++)
432 Глвва 12. Делегаты, событие, пространства имен и дополнительные элементы языка! if (а[i] temp += а [ i ] ; а - temp; // Метод выводит передаваемую ему строку в обратном порядке, static void reverse(ref string a) { string temp - int i, j; Console . WriteLine (’’Вывод строки в обратном порядке."); for(j=0, i-a.Length-1; i >- 0; i--, j++) temp += a[i]; a - temp; } public static void Main() { // Создание экземпляра делегата. strMod strOp; strMod replaceSp = new strMod(replaceSpaces) ; strMod removeSp = new strMod(removespaces); strMod reverseStr - new strMod(reverse); string str - "Проверка работы методов."; // Делегату присваиваются две ссылки. strop = replaceSp; г——-—— — ———-—\ ? Strop += reverseStr; ч----------[Создание цепочки ссылок. | // Вызов многоадресного делегата. strOp(ref str); Console.WriteLine("Преобразованная строка: " т str); Console.WriteLine() ; If Удаление из цепочки ссылок ссылки на метод replaceSpaces и добавление // ссылки на метод removespaces strop -= replaceSp; ------------------------—. strop removeSp;-4_______________Создание новой цепочки ссылок. str =• " Проверка работы методов."; // Присвоение строковой переменной str // строки "Проверка работы методов" в первоначальном виде. // Вызов многоадресного делех-ата. strop(ref str); Console.WriteLine("Преобразованная строка: ” + str); Console.WriteLine() ; } Ниже приведен результат выполнения этой программы. Замена пробелов дефисами. Вывод строки в обратном порядке. Преобразованная строка: .водотем-ытобар-акреворП Вывод строки в обратном порядке. Удаление пробелов. Преобразованная строка: .водотемытобаракреворП В методе Main () создаются четыре экземпляра делегата. Экземпляр strOp имеет значение null, поскольку при создании он не был инициирован. Остальные три
Делегаты 433 делегата ссылаются на методы, предназначенные для преобразования строки. Далее н программе создается многоадресный делегат, который затем вызывает методы : emoveSpaces () и reverse (). Это осуществляется с помощью трех операторов: .-,trOp - replaceSp; .-rOp += reverseStr; ;.trOp(ref str); I? первом операторе экземпляру strop делегата strMod присваивается ссылка на метод replaceSp. Затем с помощью оператора += к цепочке присоединяется ссылка на метод reverseStr. При вызове объекта strop вызываются оба метода. Первый метод заменяет пробелы дефисами, а второй изменяет порядок символов в строке. В операторе strop -= replaceSp; сначала из цепочки удаляется ссылка объекта replaceSp, а затем к цепочке прибав- ляется ссылка объекта removeSp: :trOp +- removeSp; Далее в программе вновь вызывается объект strOp с еще не преобразованной строкой в качестве аргумента. Теперь из строки удаляются пробелы, и она выводится в обратном порядке. Преимущества использования делегатов В предыдущем разделе описывалось использование делегатов, но не объяснялось, для чего был создан этот элемент языка. Делегаты в C# применяются по двум причинам, во-первых, они поддерживают события (которые мы рассмотрим в следующем разделе), во-вторых, они предоставляют возможность выбора вызываемого метода во время выполнения программы, а не во время компилирования. Эта способность весьма полезна, когда необходимо создание базовой конструкции (программы), в которую потом можно добавлять компоненты. Представьте себе программу для создания рисунков । подобную стандартной программе Windows Paint). В нее можно добавлять специальные световые фильтры или анализаторы изображений. Кроме того, программист может создать последовательность таких фильтров или анализаторов. При использовании делегатов эта схема модификации программы легко реализуется. g> Минутный практикум 1. Что такое делегат? Какие преимущества дает использование делегатов? Л 2. Как объявляется делегат? 3. Что такое многоадресность? 1. Делегат — это объект, имеющий ссылку на метод. Делегат позволяет выбрать вызываемый метод во время выполнения программы. 2. Делегат объявляется с помощью ключевого слова delegate, за которым указывается тип возвращаемого значения, имя делегата и список параметров вызываемых методов. 3. Многоадресность — это способность делегата хранить несколько ссылок на различные методы, что позволяет при вызове делегата инициировать эту цепочку методов.
434 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C#j События Еще одним важным элементом языка С#, построенным на основе использования й делегатов, является событие. Фактически событие — это автоматическое извещение } о каком-либо произошедшем действии. Функционируют оно следующим образом:1 - для объекта, который в соответствии с его определением (кодом) должен реагировать! на какое-то событие, регистрируется обработчик этого события. Когда событие- происходит, вызываются все зарегистрированные обработчики. Обработчики собы- .* тий создаются на основе делегатов. События являются членами класса и объявляются с использованием ключевого слова event. Общий синтаксис объявления события представлен ниже. event even t~delega се object-name; f Здесь словосочетание event-delegate — это имя делегата, используемого для'4 поддержки события, а словосочетание object-name — имя создаваемого объекта; события. Рассмотрим следующую программу: // Очень простой пример использования события. using System; // Объявление делегата, на основе которого будет определено событие, delegate void MyEventHandler () ;-<—~——-—Объявление делегата для события, j // Объявление класса, в котором инициируется событие. class MyEvent { _________________ public event MyEventHandler activate; ◄------[Объявление события | //В этом методе инициируется событие. public void fire() { if (activate !- null) «4-----—----j Определение условия для инициализации события. activate (); } class EventDemo { static void handler () { Console.WriteLine("Произошло событие."); } public static void Main() { MyEvent evt = new MyEvent(); -[ Создание экзешишpa события | // Добавление метода handler!) к цепочке обработчиков события. evt.activate += new MyEventHandler (handler) ; '<— Добавление метода к (возможной) цепочке обработчиков события. I/ Инициирование события. evt. fire () ; —---[Инициирование события. |
Событии 435 Результат выполнения этой программы следующий: ,'с.оытие произошло. Хотя данная программа достаточно проста, в ней содержатся все основные элементы, необходимые для правильной обработки события. Давайте рассмотрим ее подробнее. Программа начинается с объявления делегата для хранения ссылок на обработчики события. celeg ate void MyEventHandler(); Вес обработчики события активизируются с помощью делегата. Следовательно, делегат события определяет подпись события. В данном примере событие не имеет параметров, но их наличие допускается. Поскольку, как правило, событие является многоадресным, при его объявлении должен указываться тип возвращаемого значе- ния void. Далее в программе создается класс MyEvent, в котором объявляется событие acti- ve te и определяются условия его инициирования. Событие объявляется в следующей строке кода: pjolic event MyEventHandler activate; Обратите внимание на используемый синтаксис. Так объявляются все типы событий. Внутри класса MyEvent объявляется также метод fire (), который будет вызываться программой для инициализации события. Условие, при котором выполняется ини- циализация, определено с помощью оператора if. if(activate ! = null) activate(); Обратите внимание, что обработчик вызывается только в случае, если делегат, ассоциированный с событием activate, имеет значение, отличное от null. Внутри класса Even tDemo создается обработчик события handler (). В этом примере обработчик события просто выводит сообщение. Но можно определить обработчик гак, чтобы его операторы выполняли более содержательные действия. В методе ?<-.in() создается объект типа MyEvent, а метод handler () регистрируется как обработчик для события, объявленного в этом классе. К','Event evt = new MyEvent () ; Добавление метода handler () к цепочке обработчиков события. -L.activate new MyEventHandler(handler); Обратите внимание, что обработчик добавляется с использованием оператора +=. С обытия поддерживают только применение операторов += и -=. Наконец, происходит инициирование события. Инициирование событие. 't.fire () ; вызов метода fire() приводит к вызову всех зарегистрированных обработчиков события. В данном случае вызывается только один зарегистрированный обработчик, но их может быть и несколько.
436 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C# Широковещательное событие Как уже говорилось ранее, события создаются на основе делегатов. А поскольку делегаты могут быть многоадресными, то события могут активизировать несколько обработчиков, даже те, которые были определены в других объектах. Такие события называются широковещательными (что является синонимом слова многоадресные). Их использование позволяет множеству объектов «реагировать» на извещение о событии. Ниже приведена программа, в которой используется широковещательное событие. // В программе демонстрируется использование широковещательного события. using System; i : Объявление делегата, на основе которого будет определено событие, delegate void MyEventHandler(); // Объявление класса, в котором инициируется событие, class MyEvent { public event MyEventHandler activate; // В этом методе инициируется событие. public void fire() { if(activate !- null) activate (); } } class X { public void Xhandler() { Console.WriteLine("Событие получено объектом класса X.”}; } ) class Y { public void YhandlerO { Console.WriteLine("Событие получено объектом класса Y."); class EventDemo { static void handler () { Console.WriteLine("Событие получено объектом класса EventDemo."); } public static void MainO { MyEvent evt = new MyEvent (); X xOb = new X() ; Y yOb - new Y () ; // Добавление методов handler (), Xhandler () и YhandlerO // обработчиков события. evt.activate += new MyEventHandler(handler); evt.activate += new MyEventHandler(xOb.Xhandler); < — evt.activate += new MyEventHandler(yOb.Yhandler); в цепочку Создание цепочки обработчиков для события.
События 437 // Инициирование события. evt. fire () ; Console.WriteLine (); // Удаление одного обработчика из цепочки. evt.activate -= new MyEventHandler(xOb.Xhandler); evt. fire () ; Результат выполнения этой программы следующий: Событие получено объектом класса EventDemo. Событие получено объектом класса X. Событие получено объектом класса Y. Событие получено объектом класса Y. Событие получено объектом класса EventDemo. В этой программе создаются два дополнительных класса х и Y, в которых также определены обработчики событий, совместимые по своей подписи с типом MyEven- •_Handler. Следовательно, эти обработчики могут стать частью цепочки. Обратите внимание, что обработчики, определенные в классах х и Y, не имеют модификатора ratic. Это означает, что сначала в программе создаются объекты каждого класса, а уже затем добавляется относящийся к объекту обработчик. Необходимо понимать, что сгенерированные события передаются отдельно каждому экземпляру объекта, а не классу в целом. Следовательно, для получения извещения о событии должен быть зарегистрирован (определен) обработчик каждого объекта класса. Например, в приведенной ниже программе происшедшее событие (вызов метода fire) инициирует обработчики трех объектов класса X. / На происшедшее событие реагируют обработчики каждого из объектов класса. .sing System; • 1 Объявление делегата, на основе которого будет определено событие, delegate void MyEventHandler(); / Объявление класса, в котором инициируется событие. class MyEvent { public event MyEventHandler activate; // В этом методе инициируется событие. public void fire() { if (activate ! = null) activate (); class X { int id; public X(int x) { id = x; } public void XhandlerO { Console.WriteLine("Событие получено объектом №" + id);
438 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C#j class EventDemo { public static void Main() { MyEvent evt = new MyEvent(); X ol = new X (1); X o2 = new X (2 ) ; X o3 = new X (3) ; evt.activate +- new MyEventHandler(ol.Xhandler); evt.activate new MyEventHandler(o2.Xhandler); evt.activate +- new MyEventHandler(o3.Xhandler); // Инициирование события, evt.fire(); В результате выполнения этой программы будет выведена следующая информация: Событие получено объектом №1 Событие получено объектом №2 Событие получено объектом №3 3 Как видите, для каждого объекта отдельно регистрируется обработчик события, и f .. г- можно сказать, что каждый объект получает отдельное извещение о происшедшем событии. 1 Примечание В C# разрешается создание любого типа события. Однако для совместимости компонентов со средой .NET Framework необходимо придерживаться традиций программирования, свойственных компании Microsoft. Минутный практикум 1. Что такое событие в С#, и какое ключевое слово используется для его объявления? 2. Чем для события является делегат? 3. Могут ли события быть широковещательными? Пространства имен Еще в главе 1 мы говорили, что одним из базовых понятий C# является пространство имен. Фактически, так или иначе, пространство имен используется в каждой С#- программе. До этой главы у нас не было необходимости в деталях рассматривать пространства имен, поскольку его применение обеспечивается в С#-программах по умолчанию. 1. Событие — это извещение о каком-либо изменении в программе. Для объявления события используется ключевое слово event. 2. Делегат служит основой для создания события. 3. Да.
Пространства имен Сначала вспомним, что нам уже известно о пространстве имен. Пространство uj определяет область объявления, что позволяет хранить каждый набор имен (класс интерфейсов) отдельно от других наборов. В C# имена, объявленные в одном п] оранстве имен, не конфликтуют с такими же именами, объявленными в дру| пространстве имен. Библиотекой .NET Framework (библиотекой С#) используе пространство имен System, поэтому каждая Сопрограмма начинается строкой ^.sing System; Как вам известно из главы 11, классы, предназначенные для выполнения операг внода/вывода, определяются в пределах пространства имен, подчиненного (нахо. шсгося внутри) пространству имен System, которое называется System. Ю. Кр< того, существует множество других пространств имен, подчиненных пространс имен System, в которых хранятся другие части библиотеки С#. Пространство имен является очень важной составляющей языка С#, поскольку последние годы было создано огромное количество переменных, методов, свойст классов. В пространство имен могут быть включены библиотечные программы, кс других программистов и организаций, а также ваши собственные коды. Без при нения пространства имен попытки использования классов с одинаковыми именг приводили бы к возникновению конфликтов при компоновке программы (наприк если вы определите в своей программе класс с именем Finder, то возник конфликт с другим классом Finder, входящим в состав библиотеки, создан! другим программистом и применяемой в вашей программе). К счастью, в C# проблема решена — использование пространства имен позволяет ограничш область видимости имен, объявленных в его пределах. Объявление пространства имен Пространство имен объявляется с помощью ключевого слова namespace. Обт синтаксис объявления пространства имен показан ниже. namespace name { /I члены класса Здесь пате — название пространства имен. Все элементы, которые определ внутри пространства имен (это могут быть классы, структуры, делегаты, пере* интерфейсы или другие пространства имен) находятся в пределах области видимс этого пространства имен. Ниже приведен пример создания пространства имен Counter. В нем локализу имя — класс CountDown, используемый для реализации простого счетчика обратг отсчета. Объявление пространства имен, локализующего классы, в которых реализов различные способы подсчета чего-либо. tamespace Counter { < Объявлениепространства имен Counter?) .// Класс, предназначенный для обратного отсчета. class CountDown { int val; public CountDown(int n) { val = n; } public void reset (int n) {
440 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка val - п; public int count () { if(val > 0) return val--; else return 0; } В приведенном выше коде класс Count De wn объявляется в пределах области види- м о ст и, о п реле; i я с м ой п рост ра н ст во м и м с н С ; enter. Теперь рассмотрим программу, в которой демонстрируется использование простран- ства имен Counter. // В программе демонстрируется использование пространства имен. , using System; // Объявление пространства имен, локализующего классы, // в которых реализованы различные способы /./ подсчета чего-либо. namespace Counter { // Класс, предназначенный для обратного отсчета. * class CountDown <' > int val; public CountDown(int n) { val n; } public void reset(int n) { ч val = n; и } public int count () { if (val > 0) return val--; else return 0; } } Здесь пространство имен Counter используется } для полного определения местонахождения класса CountDown. ч class NS Demo { public static void MainO { Counter.CountDown cdl = new Counter.CountDown(10); int i; do { i = cdl.count(); Console.Write(i + " ") ; } while(i > 0); Console.WriteLine(); Counter . CountDown cd2 =- new Counter.CountDown(20); do { i - cd2.count(); Console . Write (i + " ’’) ; » while (i > C);
пространства имен 44 Console.WriteLine(); со2.reset(4); сю : i - ca2.count(> ; Console.Wrice (i + " "); i while(i > 0) ; Console.WriteLine(); релльтат выполнения программы выглядит следующим образом: >3 7 6543216 2С 13 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 С 4 2 10 Подробно проанализируем некоторые важные моменты этой программы. Посколью класс CountDown объявляется в пределах пространства имен Counter, при созданш объекта имя класса CountDown должно указываться вместе с пространством имел с-: inter, как показано ниже: Co'jr.ter. CountDown cdl — new Counter. CountDown (10) ; Однако после создания объекта нет никакой необходимости указывать пространстве имен перед именем объекта или его членов. Следовательно, метод cdl. count () можно вызывать непосредственно: 1 ceil. count () ; Директива using Как уже говорилось в главе 1, очень утомительно каждый раз указывать пространство имен, когда в программе имеют место частые обращения к членам этого пространства имен. Использование директивы usinq облегчит вам эту задачу. Для всех программ данной книги пространство имен System делается видимым путем его указания после директивы using. Следовательно, директива using может также использо- вания для того, чтобы сделать видимыми создаваемые вами пространства имен. Схшествуют две формы синтаксиса директивы using. Первая форма выглядит так: ng name; Здесь слово name указывает имя пространства имен, к которому необходимо полу- чи н> доступ. Эту форму синтаксиса директивы using вы уже встречали. Все члены, ’’прсдслснные в пределах указанного пространства имен, становятся доступными (то Сс:ь становятся частью текущего пространства имен) и могут использоваться без Полного определения местонахождения. Директива using должна быть указана в начале каждого файла, перед всеми другими объявлениями. Чиже показана новая версия предыдущей программы, в которой директива using используется дня того, чтобы сделать создаваемое пространство имен видимым. В программе демонстрируется использование пространства имен. •sing System; С помощью директивы using пространство имен Counter становится видимым для кода этой программы.
442 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка.. usinq Counter; Ч_______________Использование директивы using делает у ' видимым пространство имен Counter. // Объявление пространства имен, локализующего классы, в которых реализованы // различные способы подсчета чего-либо. namespace Counter { // Класс, предназначенный для обратного отсчета. * class CountDown ( int val; public CountDown(int n) ( val = n; } public void reset(int n) { val - n; } public int count () ( if(val > 0) return val—; else return 0; class NSDemo { public static void MainO { // Теперь класс CountDown может использоваться непосредственно без // указания его местонахождения. CountDown cdl = 'new CountDown (10) ; .4. int i; Теперь имя (класс) CountDown используется непосредственно. do { .i i = cdl. count (); a Console.Write(i + " "); } while(i > 0); Console.WriteLine(); CountDown cd2 = new CountDown(20); do { i = cd2 .count () ; Console.Write(i + " "); } whiled > 0) ; Console.WriteLine(); cd2. reset (4) ; do { i = cd2. count 0; Console.Write(i + " "); } while(i > 0); Console.WriteLine (); } ) Обратите внимание на еще один важный момент. В этой программе одно простран- ство имен не скрывает другого пространства имен. При объявлении пространства
Пространства имен 443 имен видимым оно добавляет свои имена к другим доступным в настоящий момент именам. Таким образом, оба пространства имен, System и Counter, становятся видимыми для кода этой программы. вторая форма синтаксиса директивы using Директива usrng имеет и вторую форму, показанную ниже. Gs.ng alias - name; Здесь слово alias (псевдоним) становится другим именем для класса или простран- ства имен, указываемого вместо слова пате. Далее представлена еще одна версия предыдущей программы, в которой создается псевдоним Count класса Coun- ts- .CountDown. j • в программе демонстрируется использование псевдонима, using System; // Создание псевдонима класса Counter.CountDown.__________________________ us-ng Count - Counter.CountDown; 4——--| Создание псевдонима класса Counter.CountDown. j ; i Объявление пространства имен, локализующего классы, в которых реализованы // различные способы подсчета чего-либо. namespace Counter { // Класс, предназначенный для обратного отсчета. class CountDown { int val; public CountDown(int n) { val public void reset(int n) { val - n; } public int count() { if(val > 0) return val—; else return 0; } C-tss NSDemo { public static void Main() ( Count cdl = new Count(10) int i; n; Использование псевдонима. do { i cdl.count() ; Console.Write(i + " "); } while(i > 0) ; Console.WriteLine() ; Count cd2 - new Count(20); do ( i = cd2 . count () ;
444 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка С#’ Console.Write (i * ’’ "); } while(i > C) ; Console.WriteLine(); cd2.reset(4) ; do { i --- cd2.count(); Console.Write(i + ” "); } while(1 > 0) ; Console.WriteLine(); ) После создания псевдоним Count класса Counter . CountDown можно использовать для объявления объектов без указания пространства имен. Например, строка кода Count cdl = new Count(10); создает объект типа CountDown. Аддитивность пространств имен В C# существует возможность использования одного имени для объявления более одного пространства имен. Это позволяет в одной программе делать видимыми несколько пространств имен, имеющих одинаковое имя. Например, в следующей программе определены два пространства имен Counter. В одном содержится класс CountDown, во втором содержится класс CountUp. При компилировании члены обоих пространств имен Counter становятся видимыми. //В программе демонстрируется аддитивность пространств имен. using System; // С помощью директивы using пространства имен Counter становятся видимыми // для кода этой программы. using Counter; // Объявление одного пространства имен Counter. namespace Counter { <........ ........——---------- // Класс, предназначенный для обратного отсчета, class CountDown { int val; public CountDown(int n) { val - n; } public void reset(int n) { val = n; ) В этой программе объявляются два пространства имен Counter. public int count() { if (val > 0) return val--; else return 0; } } } // Объявление еще одного пространства имен Counter. namespace Counter { <------------------------------------------ // Класс, предназначенный для отсчет^ в порядке возрастания.
445 пространства имен ,-lass CountUp { int val; int target; public inc Target get{ return target; public CountUp(int n) { target - n; val - 0; } public voiu reset(int n) { target - n; val - 0; } public int count () ( if (val < target) return val-t--1-; else return target; c.ciss NS Demo { public static void Main() { CountDown cd new CountDown(10); CountUp cu = new CountUp(8); int i; do { i = cd.count (); Console.Write (i + " ”); } while (i > 0) ; Console.WriteLine(); do { i = cu.count (); Console.Write (i + ” ") ; } whiled < cu.Target); При выполнении программы на экран будут выведены числа: - 9 876543210 С . 2 3 4 5 6 7 8 Обратите внимание, что оператор J -ng Counter; Желает видимым все содержимое пространства имен Counter. Следовательно, доступ к обоим классам CountDown и CountUp может о су шест вл яз ьс я непосредственно без Указания пространства имен. То, что пространство имен Counter было разделено на ^’•е части, не имеет значения. воженные пространства имен Пространство имен может быть вложенным. Рассмотрим следующую программу: 7' В программе демонстрируется использование вложенного пространства имен, ing System;
446 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка namespace NS1 { J class ClassA { 71 public ClassAO ( ,1 Console.WriteLine("Оператор конструктора класса ClassA."); S ) Пространство имен NS2 является нло- j —---женным в пространство имен NS1. [ >! namespace NS2 { // Объявление вложенного пространства имен. class ClassB { public ClassB () ( -.J Console.WriteLine("Оператор конструктора класса ClassB."); 1 1 1 ) ’ > ) J class NestedNSDemo { л! public static void Main() { NS1. ClassA a- new NS1. ClassA () ; // NS2.ClassB b = new NS2.ClassB(); // Ошибка!!! Местоположение пространства* // имен NS2 не указано полностью, J / / поэтому оно не будет видимо для // кода программы. 1 NS1.NS2.ClassB b = new NS1.NS2.ClassB(); // Это правильное полное // определение местонахождения класса ClassB. ‘ Л } ; 1 J Результат выполнения программы будет следующим: В * * 11 Оператор конструктора класса ClassA Оператор конструктора класса ClassB. В этой программе пространство имен NS2 является вложенным в пространство име4 NS1. Поэтому для получения доступа к классу ClassB необходимо полностью указать его местоположение, то есть, используя операторы точка (.), указать оба пространства имен — пространство имен верхнего уровня NS1 и вложенное NS2. Вложенное пространство имен можно объявить в одной строке кода, используя одно ключевое слова namespace и разделяя пространства имен оператором точка. Напри- мер, фрагмент кода namespace OuterNS { namespace InnerNS { II... } } можно переписать так: namespace OuterNS.InnerNS {
447 Пространства имен пространство имен, используемое по умолчанию fc;ut в программе не объявляется пространство имен, то используется пространство имен, определенное по умолчанию. Поэтому в предыдущих программах вам не нужно было использовать ключевое слово namespace. Пространство имен по умолчанию учебно применять для коротких простых программ, но код больших прикладных » программ должен содержаться в каком-либо пространстве имен. Код необходимо инкапсулировать в пространство имен для предотвращения конфликтов имен. Пространство имен — это один из элементов языка, помогающий в организации профамм и делающий их жизнеспособными в современном сложном сетевом окру- жении. $ Минутный практикум I. Что определяет пространство имен? Г у 2 Напишите две формы синтаксиса использования директивы using? 3. Могут ли два пространства имен с одинаковыми именами использоваться в одной) программе? Проект 12-1. Помещение класса Set в пространство имен 0 Set.cs, SetDemo.cs В проекте 7-1 был создан класс Set, в котором имитировалось множество. Теперь необходимо поместить этот класс в собственное пространство имен, поскольку он может конфликтовать с другими классами, имеющими такое же имя. Пошаговая инструкция I. Используя файл SetDemo.cs из проекта 7-1, перенесите весь код класса Set в файл Set.cs. При этом поместите этот код в пространство имен MyTypes.Set, как показано ниже: /* Проект 12-1 Помещение класса Set в пространство имен. */ using System; namespace MyTypes.Set { class Set { char[] members; // Массив, содержащий символы (имитирующий множество), int len; // Количество элементов множества. I Пространство имен определяет область видимости. - using name и using alias - name. V Да.
448 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка Ci // Создание пустого множества. « public Set() { | len = 0; Ж } Ж // Создание пустого множества заданного размера. Ч public Set(int size) { | members = new char[size]; // Выделение памяти для множества. len =0; // Элементы не были сконструированы. } // Конструирование множества на базе другого множества. public Set(Set s) { members - new charts.len]; // Выделение памяти для множества. for(int i-0; i < s.len; i++) membersfi] = s[i]; , len = s.len; // Количество элементов множества. ; } // Определение свойства Length, предназначенного только для чтения. public int Length { get { return len; } 1 [ // Определение индексатора, предназначенного только для чтения. public char this[int idx]{ get { if(idx >= 0 & idx < len) return members[idx]; else return (char)0; /* Если элемент входит в состав множества, метод find возвращает индекс элемента, если не входит — возвращается значение -1. */ int find(char ch) { int i; for (i-0; i < len; i + + ) if (members [i] ch) return i; return -1; } // Добавление уникального элемента в множество. public static Set operator +(Set ob, char ch) { Set newset - new Set(ob.len + 1); // Увеличение числа элементов // нового множества на единицу. // Копирование элементов. for(int i=0; i < ob.len; i++) newset.members[i] = ob.members[i]; // Переменной len возвращаемого объекта присваивается значение // переменной len копируемого объекта, то есть передается размер
Пространства имен 449 // множества. newset.len - ob.len; // Выполняется проверка - имеется ли добавляемый символ в множестве, if(ob.find(ch) — -1) { // Если элемент не найден, // он добавляется в новое множество. newset.members -newset.len] - ch; newset.len++; } return newset; // Возврат нового множества (объекта класса Set). } // Удаление элемента из множества. public static Set operator - (Set ob, char ch) ; Set newset - new Set(); int i - ob.find(ch); // Если элемент не найден, // переменной г присваивается значение 1. // Копирование оставшихся элементов с использованием перегруженного // оператора -г . for(int j=0; j < ob.len; j-r-r) if(j !— i) newset - newset + ob.members[j]; return newset; } // Объединение множеств. public static Set operator + (Set obi, Set ob2) « Set newset = new Set (obi); // Копирование первого множества. // Добавление уникальных элементов из второго множества. for(int i-0; 1 < ob2.1en; 1++) newset newset + ob2[i]; return newset; // Возврат новою множества. // Разность множеств. public static Set operator -(Set obi, Set ob2) { Set newset — new Sec(obi); // Копирование первого множества. // Вычитание из первого множества элементов второго множества. for (int i^O; i < ob2.1en; i-«—-) newset - newset - ob2[i]; return newset; // Возврат нового множества. } } } 2. После помещения класса Set в пространство имен М-,'Types. Set для использо- вания этого класса необходимо включить пространство имен MyTypes.Set в программу, как это показано ниже: using MyTypes.Set;
лава к. делегаты, сооытия, пространства имен и дополнительные элементы языка 3. Если этого не сделать, в программе необходимо будет каждый раз при обращении?; к классу set полностью указывать его местоположение, как показано в следую. щей строке кода: MyTypes.Set.Set si =- new MyTypes.Set.Set(); Операторы преобразования Иногда возникает необходимость использования объектов класса в выражениях содержащих другие типы данных. Для решения этой задачи можно использовать перегрузку одного или нескольких операторов. Но чаше всего необходимо простое преобразование из одного типа класса в другой. Для этого в C# предусмотрена возможность создания операторов преобразования, с помощью которых объект соз- данного вами класса можно преобразовать в другой тип. Существуют две формы синтаксиса операторов преобразования — неявная и явная. Они показаны ниже: public static operator implicit target-type(source-type v) {return value;) public static operator explicit target-type(source-type v) {return value;) Здесь словосочетание target-type — это тип, в который преобразуется объект (конечный тип), словосочетание source-type — исходный тип, элемент v — ссы- лочная переменная, которая ссылается на объект, а слово value — значение объекта после преобразования. Операторы преобразования возвращают данные только типа target-type. Если при определении оператора указано неявное преобразование (implicit), то преобразование инициируется автоматически, например, при использовании объекта в выражении с переменной, имеющей тип target-type (то есть тип, в который и преобразовывался объект при определении оператора преобразования). Если при определении оператора указано явное преобразование (explicit), то преобразова- ние инициируется при использовании приведения типов. Одной паре (объекту исходного типа и типа, в который преобразуется объект) нельзя задавать два опера- тора преобразования, для одного из которых было бы указано явное преобразование, а для другого неявное. Чтобы продемонстрировать работу оператора преобразования, используем класс ThreeD, созданный в главе 7. Вспомним, что класс ThreeD хранит координаты некоторой точки в трехмерном пространстве. Предположим, что необходимо преоб- разовать объект типа ThreeD в целочисленный тип таким образом, чтобы при его использовании в целочисленном выражении содержалось значение, равное произве- дению значений всех трех координат. Для этого используем оператор неявного преобразования: public static implicit operator int(ThreeD opl) { return opl.x w opl.у * opl.г; } Далее приведена программа, в которой используется оператор преобразования. // В программе демонстрируется использование оператора преобразования, using System;
Операторы преобразования 451 // Класс, содержащий координаты объекта в трехмерном пространстве. e<ass ThreeD ( int х, у, z; // Координаты объекта. punlic ThreeDO ( х =- у z - 0; } public ThreeDfint i, int j, int k) { x = i; у - j; z •= k; } Перегрузка бинарного оператора + для пары операндов, • имеющих тип ThreeD. public static ThreeD operator +(ThreeD opl, ThreeD op2) ThreeD result - new ThreeDO; result.x = opl.x + op2.x; result.у - opl.у + op2.y; result.z opl.z + op2.z; return result; / Определение оператора, для которого указано неявное преобразование из / типа ThreeD в тип int. public static implicit operator int(ThreeD opl) return opl.x * opl.у * opl.z; Оператор, выполняющий преобра- зование из типа ThreeD в тип int. // Отображение координат X, Y, Z. public void show() t Console .WriteLine (x + ” + у + ", " + z) ; class ThreeDDemo { public static void Main() { ThreeD a ~ new ThreeD(1, 2, 3); ThreeD b = new ThreeD(10, 10, 10); ThreeD c = new ThreeDO; int i; Console . Write (’’Координаты объекта a: "); a.show (); Console.WriteLine(); Console .Write ("Координаты объекта b: ’’) ; b.show(); Console.WriteLine (); c - a + b; // Сложение объектов а и b. Console.Write("Результат выполнения операции сложения " + "объектов а и b: "); с . show () ; г——-—— ----—-----—----—, _ . г . , . , ч Инициируется преобразование Console.WrxteLine£H_---)ВТИ„РУ г - а; // При выполнении этого оператора присваивания инициируется // неявное преобразование объекта а в тип int.
452 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка Console-WriteLine("Результат выполнения выражения i Инициируется преобразование | Выполняется неявное преобразование Console-WriteLine("Результат выполнения выражения а а: ” + 1) ; двух объектов а и Ь в * 2 - Ь: " t- 1) ; При выполнении программы будут получены следующие результаты: Координаты объекта а: Координаты объекта Ь: 10, 10z 1С Результат выполнения операции сложения объектов а и Ь: 11, 12, 13 Результат выполнения выражения i - а: б Результат выполнения выражения а * 2 - b: -у88 Как показано в программе, при использовании объекта типа ThreeD в целочисленном выражении, таком как i = а, инициируется оператор неявного преобразования. В данном конкретном случае при выполнении оператора преобразования возвраща- ется значение 6, которое является произведением значений координат, хранящихся в объекте а. Однако если выражение не требует преобразования в тип int, оператор преобразования не вызывается. Поэтому при выполнении выражения с = а + ь вызов оператора преобразования operator int () не требуется. Помните, что для решения различных задач можно создавать различные операторы преобразования. Например, можно определить оператор для преобразования в тип double или long. Каждый такой) оператор применяется автоматически и независимо от остальных. Оператор неявного преобразования применяется автоматически в следующих слу- чаях: когда преобразование требуется в выражении, объект передастся методу, вы- полняется оператор присваивания, используется явное приведение типов. В качестве альтернативного можно создать оператор явного преобразования, который применя- ется только при использовании явного приведения типов и нс вызывается автомати- чески. Например, ниже приведена переработанная предыдущая программа, в которой используется явное приведение к типу int. // В программе демонстрируется использование оператора явного // преобразования. using System; ' ! Класс, содержащий координаты объекта в трехмерном пространстве, class ThreeD { int х, у, z; / Координаты объекта. public ThreeD() { х - у - z - 0; } public ThreeD(int i, int j, inc k) { x - 1; у - j; z = k; } / / Перегрузка бинарного оператора для пары операндов, // имеющих тип ThreeD. pubnc static ThreeD operator + (ThreeD opl, ThreeD op2)
Операторы преобразования ThreeD result - new ThreeDO; result.x = opl.x + op2.x; result.у -= opl.у + op2.y; result.z ~ opl.z + op2.z; return result; Теперь определяется оператор явного преобразования. ______________ public static explicit operator int (ThreeD opl) -4---------Теперь преобразование явное. return opl.x * opl.у * opl.z; '/ Отображение координат X, Y, Z. public void show О t Console .WriteLine (x + ", " +- у * ", ” + z) ; c.-.ass ThreeDDemo < public static void Main() { ThreeD a -= new ThreeD (1, 2, 3) ; ThreeD b =- new ThreeD(10, 10, 10); ThreeD c - new ThreeDO; int i; Console . Wri te ("Координаты объекта a: ’’); a.show(); Console.WriteLine(); Console . Write ( "Координаты объекта b: "); b.show(); Console.WriteLine(); с -= a + О; // Сложение объектов а и b. Console. Write (’’Результат выполнения операции сложения ” + "объектов а и b: "); с.show (); Console.WriteLine(); i = (int) a; // Для выполнения этого оператора присваивания требуется // выполнение операции приведения типа, после чего будет вызван J / оператор явного преобразования объекта а в тип int. Console.WriteLine("Результат выполнения выражения 1 - а: ” т 1); Console.WriteLine (); Теперь ipcoyeiCH выполнение операции приведения типа. i — (int) а * 2 - (int)b; // Требуется выполнение операции приведения типа. Console.WriteLine("Результат выполнения выражения а * 2 - Ь: " + 1) ;
454 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C# Поскольку теперь оператор преобразования указан с ключевым словом explicit для его инициации и преобразования объекта класса ThreeD в тип int требуется’' выполнение операции приведения типа. Например, если в операторе ; 1 (inc) а; > удалить операцию приведения типа, программа нс будет скомпилирована. i 1 профессионала — ~ - ~ Вопрос. Если оператор неявного преобразования вызывается автоматически без использования операции приведения типа, зачем нужно создавать оператор явного преобразования? Ответ. Хотя неявное преобразование удобно в использовании, вы должны применять его только тогда, когда уверены, что преобразование не приведет к 3' возникновению ошибки. Оператор неявного преобразования можно создавать только в случае выполнения двух условий. Во-первых, при преобразовании не должна быть ж потеряна информация, что происходит при усечении, переполнении или потере; знака. Во-вторых, преобразование не должно приводить к возникновению исключи-' тельной ситуации. Если не соблюдается хотя бы одно из этих условий), нужно использовать явное преобразование. ! Для использования операторов преобразования существуют некоторые ограничения.? О Нельзя создавать оператор, который выполнял бы преобразование из встроенного? типа в какой-либо другой. Например, нельзя переопределить операцию приведе-1 ния типа double к типу int. О В операторе преобразования нельзя использовать тип object. < О Нельзя определять неявное и явное преобразование для одной пары типов, Ц состоящей из исходного и конечного типов. S О Нельзя определять преобразование из наследуемого класса в наследующий. * О В операторе преобразования нельзя использовать интерфейс. £ Минутный практикум I Для чего предназначен оператор преобразования? 3 отличается оператор явного преобразования от оператора неявного преобразования? 3. Можно ли использовать оператор преобразования для преобразования из встроенного типа? 1. Оператор преобразования предназначен для преобразования в тип класса пли из типа класса. 2. Оператор неявного преобразования вызывается автоматически. Оператор явного преобра- зования должен вызываться с использованием операции приведения типа. 3. Нет.
Препроцессор 455 Препроцессор В C# определены несколько директив препроцессора, которые влияют на способ интерпретации компилятором исходного файла программы. Эти директивы изменя- ют текст исходного файла, в котором они встречаются, перед трансляцией программы в объектный код. Директивы препроцессора в основном пришли в C# из C++, фактически препроцессоры в этих языках очень похожи. Данные инструкции полу- чили название директив препроцессора, поскольку они традиционно обрабатывались котельной фазе компилирования, которая называлась препроцессором. Современные технологии компилирования больше не требуют отдельной фазы для обработки директив, но имя этих инструкций осталось прежним. В C# определены следующие директивы препроцессора. ikicfine #elif #else #endif #endregion terror frif ftline #region #undef ^warning Вес директивы препроцессора начинаются co знака #. Кроме того, каждая из них должна находиться на отдельной строке программы. Современная объектно-ориентированная архитектура C# не так нуждается в дирек- тивах препроцессора, как архитектура более старых языков, но иногда директивы препроцессора могут быть полезны, особенно для условной компиляции. Далее по порядку будут рассмотрены все директивы препроцессора. Директива препроцессора #define С помощью директивы #define определяется символьная последовательность, ко- торая называется символьной константой (symbol). Наличие или отсутствие конкрет- ной символьной константы определяется с помощью директивы #if или #elif и может использоваться для управления компилированием. Ниже приведен общий синтаксис директивы #define. ^define symbol Обратите внимание, что в этом операторе отсутствует точка с запятой. Между Директивой #define и символьной константой может быть произвольное количество пробелов, а признаком окончания символьной константы является символ новой строки. Например, для определения символьной! константы experimental необхо- димо использовать директиву S-Tefine EXPERIMENTAL Вопрос. Я знаю, что в C++ директиву #define можно использовать для выполнения текстовых подстановок, например, определения имени для значения или Для создания макросов, аналогичных функциям. Поддерживаются ли в C# такие свойства директивы #define? Ответ. Нет. В C# директива #define используется только для определения символьной константы.
456 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C# Директивы препроцессора #if и #endif Использование директив препроцессора #if и tendif позволяет условно компили- ровать последовательность кода. Их выполнение ивисит от результата оценки выражения, включающего одну или более символьных констант. Выражение может принимать значение t:ue или !<use. Символьная константа принимает значение true, если она определена директивой rdefine. Общий синтаксис директивы #if следующий: #if symbol~expression statement sequence frendif Если выражение, следующее за директивой #rf, принимает значение true, то код, находящийся между директивами trf и #endif, будет скомпилирован. В противном случае этот код пропускается компилятором. Наличие директивы #endrf указывает па окончание блока операторов директивы #if. Символьное выражение может быть простым и состоять из одной символьной константы. В символьном выражении могут использоваться операторы !, ==, !=, && и | . Также разрешается применение круглых скобок. Рассмотрим небольшую программу. // В программе демонстрируется использование директив препроцессора. // #if, #endif и #define. ^define EXPERIMENTAL using System.; class Test { public static void MainO { Этот оператор будет скомпилирован, поскольку символьная константа experimental определе- на директивой #define. #if EXPERIMENTAL Console.WriteLine("Этот оператор будет скомпилирован только при" + "Хпналичии в программе определенной символьной константы EXPERIMENTAL."); #endif Console.WriteLine("Этот оператор будет скомпилирован независимо от " + "определения символьной константы."); В результате выполнения программы будут выведены следующие строки: Этот оператор будет скомпилирован только при наличии в программе определенной символьной константы EXPERIMENTAL. Этот оператор будет скомпилирован независимо от определения символьной константы. В этой программе определена символьная константа experimental. Следовательно, когда в коде программы встречается директива Of, символьное выражение прини- мает значение true и компилируется первый оператор WriteLine () ;. Если удалить определение символьной константы experimental и повторно скомпилировать программу, то первый оператор WriteLine () ; скомпилирован не будет, поскольку выражение после директивы #if примет значение false. Второй оператор
Препроцессор 457 . .'iteLine () ; будет скомпилирован в любом случае, поскольку он нс является частью блока t т 1. Как уже говорилось, в блоке Sil можно использовать символьное выражение. Например, в следующей программе условием для компиляции одного из операгоров является наличие двух определенных символьных констант. В программе демонстрируется использование символьного выражения, '.efine EXPERIMENTAL s jefine TRIAL ing System; _.ass Test ( public static void Main() { #if EXPERIMENTAL Console.WriteLine("Оператор будет скомпилирован в той версии " - "программы, в которой будет определена символьная константа EXPERIMENTAL."); #endif #if EXPERIMENTAL && TRIAL <-----[Символьное выражение, j Console.Error.WriteLine("Оператор будет скомпилирован а той версии " т "программы, в которой будут определены две символьные константы ’* + "EXPERIMENTAL и TRIAL."); #endif Console.WriteLine("Этот оператор будет скомпилирован независимо от " + "определения символьных констант."); Результат выполнения программы следующий: I. ератор будет скомпилирован в той версии программы, а которой будет определена символьная константа EXPERIMENTAL. Оператор будет скомпилирован в той версии программы, в которой будут определены две символьные константы EXPERIMENTAL и TRIAL. "-от оператор будет скомпилирован независимо от определения символьных * нстант. В программе определены две символьные константы — exe-^erihenlal и trial. Вгорой оператор WriteLine () ; будет скомпилирован только в случае определения обеих символьных констант. Директивы препроцессора #else и #elif В C# директива Seise работает так же, как оператор else. С ее помощью указывается а-’.ьтернативный вариант блока операторов, который будет скомпилирован, если Результатом оценки выражения директивы Srf будет значение false. Предыдущая программа может быть переписана следующим образом: В программе демонстрируется использование директивы препроцессора #else. define EXPERIMENTAL ’-sing System;
458 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C# class Test { public static void MainO { #if EXPERIMENTAL Console.WriteLine("Оператор будет скомпилирован в той версии ” + "программы, в которой будет определена символьная константа EXPERIMENTAL.”); # е 1 sе *<— ----j Использованиедирективы Seise, j Console.WriteLine("Альтернативный вариант оператора."); tfendif # if EXPERIMENTAL && TRIAL Console.Error.WriteLine("Оператор будет скомпилирован в той версии " + "программы, в которой будут определены две символьные константы " + "EXPERIMENTAL и TRIAL."); # е 1 sе ———-----р/)снользованисдирективы #е 1 seT| Console.Error.WriteLine!"Еще один альтернативный вариант оператора."); frendif Console .WriteLine ("Этот оператор будет скомпилирован независимо от ’’ + "определения символьных констант."); } } В этой программе из пяти операторов Console.WriteLine(); компилируются только три. В результате выполнения этих операторов на экран выводятся следующие строки: Оператор будет скомпилирован в той версии программы, в которой будет определена символьная константа EXPERIMENTAL. Еще один альтернативный вариант оператора. Этот оператор будет скомпилирован независимо от определения символьных констант. Поскольку в этой версии программы символьная константа TRIAL не определена,, компилятор будет использовать код, указанный для второй директивы #else. Обратите внимание, что наличие в коде программы директивы #else означает как конец блока операторов, относящихся к директиве #if, так и начало блока операто- ров, относящихся к директиве #else. Это необходимо, поскольку с директивой #if может ассоциироваться только одна директива #endif. Предназначение директивы #elif аналогично предназначению оператора else if- Она используется для создания цепочки if-else-if, с помощью которой можно определить несколько вариантов компиляции программы. За директивой #elif следует символьное выражение. Если выражение принимает значение true, то компилируется блок кода, идущий за символьным выражением, а выражения, . указанные за другими директивами #elif, не проверяются. Если выражение прини--л мает значение false, то проверяется следующее условное выражение в цепочке. Ц Общая форма синтаксиса директивы #elif выглядит так: # i f symbol-express!on statement sequence #elif symbol-expression statement sequence #elif symbol-expression statement sequence #elif symbol-expression statement sequence i
Директива препроцессора tfundef 45 symbol-expression statement sequence frondif Например, ! в программе демонстрируется использование директивы препроцессора #elif. flacline RELEASE Going System; class Test { public static void Main() { #if EXPERIMENTAL Console.WriteLine("Оператор будет скомпилирован в той версии " + ’’программы, в которой будет определена символьная константа EXPERIMENTAL."); #elif RELEASE -----------| Использование директивы #elif | Console.WriteLine("Оператор будет скомпилирован в той версии " + "программы, в которой будет определена символьная константа RELEASE."); frelse Console.WriteLine("Оператор будет скомпилирован в той версии " + "программы, в которой не будет определена ни олна из перечисленных " + "выше констант. #endif #if TRIAL && ’RELEASE Console.WriteLine("Оператор будет скомпилирован в той версии " + "программы, в которой будет определена символьная константа TRIAL."); frendif Console.WriteLine("Этот оператор будет скомпилирован независимо от " + "определения символьных констант."); Программа выводит на экран следующие строки: Ci.оратор будет скомпилирован в той версии программы, в которой будет 0 .ре делена символьная константа RELEASE. ° 'от оператор будет скомпилирован независимо от определения символьных констант. препроцессора #undef С помощью директивы препроцессора #undef удаляется данное ранее определение символьной константы, которая указывается за директивой. То есть далее в програм- ме указанная символьная константа не будет считаться определенной. Общий син- таксис директивы ttundef следующий: *undef symbol
460 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка С? Например, в таком фрагменте кода: ^define SMALL frif SMALL #undef SMALL // С этого места программы символьная константа SMALL не считается / - определенной. После указания в программе директивы sundef символьная константа small не считается определенной. Директива используется в основном для локализации символьной константы в тех частях кола, где она необходима. Директива препроцессора #еггог С помощью директивы препроцессора Jferror компилятор получает команду оста- новить компилирование. Эта директива используется для отладки программы. Ее общий синтаксис выглядит так: terror error-message Если в коде программы встречается директива #еггог, на экран выводится сообще- ние об ошибке. Например, если компилятор обнаружит в коде строку ' ferror Далее в программе возникает ошибка: компилирование останавливается, и на экран выводится сообщение Далее в про- грамме возникает ошибка!. Директива препроцессора #warning Директива ^warning аналогична директиве terror за исключением того, что при ее использовании предупреждающее сообщение выводится, но компилирование не останавливается. Общий синтаксис директивы «warning следующий: ^warning warning-message Директива препроцессора #line Директива препроцессора # line используется для указания номера строки, с которой будут нумероваться строки следующего за директивой кода, и имени файла, в который будут направляться все сообщения, возникающие при компиляции данного кода. Общий синтаксис директивы #lir,e следующий: #line number "filename" Здесь элемент number —любое целое число, которое становится номером следующей за директивой строки, а необязательная строка filename — это любой действитель- ный идентификатор файла, в который будут направляться сообщения, возникающие
Атрибуты —— ------------- при компиляции. Директива #1 и н специальных приложениях. 41 пе в основном используется для отладки программ директивы препроцессора #region и #endregion Используя директивы iregicn и «endregion, можно определить фрагмент код; который будет свернут или вновь развернут е помощью средств интегрированно среды разработки Visual C++ IDE. Общий синтаксис использования этих директи представлен ниже: #region // code sequence #e.r*dregion ю Минутный практикум Т’Пк 1- Какая директива препроцессора используется для определения символьной \ ’1 константы? 2. Для чего применяется директива препроцессора #if? 3. Какие операторы разрешено использовать в символьных выражениях, ассо- циированных с директивой #if или #elif? Атрибуты В C# есть возможность добавлять в программу описательную информацию в форме атрибута. С помощью атрибута определяется дополнительная информация, которая ассоциируется с классом, структурой, методом и так далее. Например, можно создать атрибут, в котором указывается тип кнопки, отображаемой классом. Атрибуты определяются в квадратных скобках перед элементом, для которого они создаются. Вы можете определить собственные атрибуты либо использовать атрибу- ты. определенные в С#. Создание собственных атрибутов в этой книге нс рассмат- ривается, а встроенные в C# атрибуты Conditional и Obsolete вы можете успешно использовать. | Атрибут Conditional 1 Афибут Conditional является, возможно, одним из наиболее часто используемых и С#. Он позволяет создавать условные методы. Условный метод выполняется только 15 том случае, если указанная в атрибуте символьная константа определена с помощью Директивы #define. В противном случае метод выполняться не будет. То есть использование условного метода является альтернативой! условному компилированию 1- Для определения символьной константы используется директива препроцессора ffdefine. 2- Директива препроцессора #if применяется для определения блок кода, который будет скомпилирован, если символьное выражение, ассоциированное с этой директивой, прини- мает значение true. В завершении этого блока кода (операторов) указывается директива #endif. 3. В символьных выражениях, ассоциированных с директивой #if или #elif, разрешено использование следующих операторов: - I 1 и !
462 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C# с помощью директивы #if. Для использования атрибута Conditional нсоб,ходимо включить в программу пространство имен System.Diaancstics. Рассмотрим следующий пример: // В программе демонстрируется использование условного атрибута Conditional. V ^define TRIAL using System; using System.Diagnostics; class Test { Conditional ("TRIAL") j void trial () { Указание в условном атрибуте символьной константы TRIAL. Console.WriteLine("Выполнение этого метода зависит от определения символьной" + " константы TRIAL."); } [Conditional("RELEASE") ) < void release() { Указание в условном атрибуте символьной константы RELEASE. Console.WriteLine("Выполнение этого метода зависит от определения символьной” + ” константы RELEASE. "); } public static void Main() { Test t = new Test(); t.trial(); // Метод будет выполнен только в том случае, если в // программе была определена символьная константа TRIAL. t.release(); // Метод будет выполнен только в том случае, если в // программе была определена символьная константа RELEASE. При выполнении программы будет вызван метод, выводящий следующую строку: Вызов этого метода зависит от определения символьной константы TRIAL. Внимательно рассмотрим эту программу, чтобы понять, почему в результате выво- дится именно эта строка. Прежде всего, отметим, что в программе определена символьная константа trial. Далее обратим внимание на то, как кодируются методы trial () и released • Для обоих методов указан атрибут Conditional, имеющий такой синтаксис: [Conditional symbol] Здесь элемент symbol — это символьная константа, от определения которой зависит, будет ли выполняться данный метод. Этот атрибут может использоваться только с методами. Если символьная константа определена, то при вызове метода его код будет выполняться. Если символьная константа не определена, то метод выполняться не будет. Внутри метода Main () вызываются оба метода, trial () и release ().Но в програм- ме определена только символьная константа trial, следовательно, выполняется только метод trial (). Вызов метода released игнорируется. Если определить символьную константу release, то метод release О тоже будет выполняться. Если
Атрибуты 463 определение символьной константы trial будет удалено, метод trial () выполнять- ся нс будет. условные методы имеют некоторые ограничения. О Для них должен быть указан тип возвращаемого значения void. О Они должны быть членами класса, а не интерфейса. О Перед ними не может стоять ключевое слово override. Атрибут Obsolete Агрибут System.Obsolete позволяет отмечать элемент программы как устаревший. Эго атрибут имеет следующий синтаксис: obsolete ’’message”} Здесь словом message обозначается сообщение, которое будет выведено на экран при компилировании данного элемента программы. Ниже приведена небольшая программа, в которой используется атрибут System.Obsolete. 1 В программе демонстрируется использование атрибута Obsolete. using System; ______________________________________ В атрибуте указывается строковый литерал, который будет выведен при компилировании метода rnyMeth {). c.ass Test { [Obsolete("Вместо этого метода следует использовать метод myMeth2.")} static int rnyMeth(int a, int b) { return a / b; } // Усовершенствованная версия метода rnyMeth. static int myMeth2(int a, int b) { return b =— 0 ? 0 : a /b; public static void MainO { // При компилировании этого оператора будет выведено сообщение. Console.WriteLine ("4 / 3 = ’’ + Test.rnyMeth(4, 3) ) ; // При компилировании этого оператора сообщение, указанное в атрибуте, // выведено не будет. Console.WriteLine (’’4 / 3 ~ ’’ + Test .myMeth2 (4 , 3)); Нели при компилировании программы в методе Main() встретится вызов метода "'/Metho , будет выведено сообщение, рекомендующее программисту использовать метод myMeth2 О .
464 Глава 12. Делегаты, событие, пространства имен и дополнительные элементы языка C# Минутный практикум Л1 2 3 ) | I. Что такое атрибут? Л %. у 2. Что такое условный метод? Л 3. Какие функции выполняет атрибут Obsolete? Небезопасный код C# позволяет писать так называемый небезопасный код. Небезопасным является не “ тот код, который плохо написан, а тот, выполнение которого не управляется CLR. J Как уже говорилось в главе 1, язык C# обычно используется для создания управляе- мого кода. Однако существует возможность написать код, выполнение которого не V, контролируется CLR. Поскольку неуправляемый код не подвергается такому контро- | лю и ограничениям, как управляемый код, он называется небезопасным. При его л использовании нельзя быть уверенным, что он не произведет какие-либо вредные £ действия. Следовательно, термин небезопасный не означает, что код некорректный по ,* существу, имеется в виду лишь то, что этот код потенциально может выполнять $ действия, не контролируемые CLR. Конечно, применение управляемого кода предпочтительнее, но он не позволяет использовать указатели. Если вы знакомы с языком С или C++, то знаете, что Ц указатели — это переменные, хранящие адреса других объектов. Следовательно, .> указатели имеют некоторое сходство со ссылками в С#. Основное различие между ® ними заключается втом, что указатель может содержать любой адрес памяти, а ссылка ® всегда содержит адрес памяти, выделенной для объекта ее типа. Поскольку указатель может ссылаться на любой адрес памяти, существует возможность его некорректного ж использования. Кроме того, при использовании указателей можно допустить ошибку в коде. Поэтому в C# не поддерживается использование указателей при создании управляемого кода. Однако они полезны и даже необходимы для некоторых типов е программ (например, для утилит системного уровня). Все операции с указателями jt должны быть отмечены как небезопасные, поскольку они выполняются за пределами ‘ .1 управляемой среды. л В общем, если вам необходимо создать код, который выполняется за пределами CLR, у лучше использовать язык C++. В C# указатели объявляются и применяются также, как и в языках C/C++. Но основная цель C# — создание управляемого кода, а I способность этого языка поддерживать неуправляемый код используется для решения особых задач. Чтобы скомпилировать неуправляемый код, необходимо применить опцию компилятора /unsafe. Работа с небезопасным кодом — это отдельная сложная тема, которая в данной книге рассматриваться не будет. Но все же мы коротко расскажем об использовании указателей и двух ключевых слов, unsafe и fixed, применяющихся для поддержки неуправляемого кода. 1. Атрибут — это дополнительная информация, ассоциированная с каким-либо элементом (классом, структурой, методом). 2. Условным называется метод, для которого определен атрибут Conditional. Условный метод выполняется только в том случае, если была задана указанная в атрибуте символьная константа. 3. Атрибут Obsolete инициирует при компиляции вывод предупреждающего сообщения об использовании устаревшего элемента (чаще всего это касается метода). L
Небезопасный код 465 Краткая информация об указателях указатели — это переменные, которые хранят адреса других объектов. Например, если переменная х содержит адрес объекта у, то говорят, что переменная х «указы- вает» на объект у. Общий синтаксис объявления указателя следующий: „ре* var-name Здесь слово type — это тип указателя, который не должен быть типом класса. Словосочетание var-name — это имя указателя. Например, для объявления пере- менной р как указателя на объект типа int используется оператор rut* гр; Для указателя с типом float используется оператор float* fp; В полом, если в операторе объявления перед именем переменной стоит знак *, такая переменная становится указателем. Тип данных, на который ссылается указатель, определяется его типом. Следователь- но, в предыдущих примерах указатель ip может использоваться для ссылки на объект типа int, а указатель fp — для ссылки на объект типа float. Именно потому, что указатели могут содержать адрес любой области памяти, они являются потенциально небезопасными. Существуют два оператора для работы с указателями — * и &. Оператор & является унарным оператором, который возвращает адрес области памяти, выделенной для его операнда. (Вспомним, что для унарного оператора требуется только один операнд.) Например, в результате выполнения фрагмента кода mt* ip; int num = 10; ip - &num; переменной гр будет присвоен адрес области памяти, выделенной для переменной пгт. Этот адрес указывает местоположение переменной в оперативной памяти компьютера, но он не имеет ничего общего со значением переменной num. Следова- тельно, указатель 1р не хранит значение 10 (начальное значение переменной num), он только содержит адрес, по которому хранится эта переменная. Можно сказать, что оператор & возвращает адрес переменной, перед именем которой он использу- ется. Таким образом, оператор ip = snure; можно прочитать так: «Переменная ip получает адрес переменной num». Оператор * является дополнением к оператору &. Это унарный оператор, возвращаю- щий значение переменной, адрес которой содержится в его операнде. Например, если переменная гр хранит адрес переменной num, то во фрагменте кода val; val — *ip; переменной val будет присвоено значение 10, то есть значение переменной num, на которую ссылается указатель гр. Можно сказать, что оператор * возвращает значе- ние, хранящееся по адресу, содержащемуся в операнде. В этом случае оператор val - *гр; можно прочесть следующим образом: «Переменная val принимает значе- ние, хранящееся по адресу, который содержится в переменной гр».
466 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C# Указатели могут также использоваться со структурами. При обращении к члену структуры с помощью указателя нужно использовать оператор -> вместо оператора точка (.). Например, определим простую структуру: struct MyStruct { public int x; public int y; public int sum() { return x + y; }; Доступ к членам структуры с использованием указателей должен выполняться следующим образом: MyStruct о = new MyStruct О; MyStruct* р; // Объявление указателя. р — & о ; р~>х - 10; р~>у == 20; Console.WriteLine("Сумма равна: " + p->sum()); уъ~0/пве/гш профессионала Вопрос. При объявлении указателя в C++ действие оператора * не распро- страняется на весь список переменных в операторе объявления. Следовательно, в операторе int* р, q; объявляется указатель р с типом int и целочисленная переменная q. То же самое происходит и в следующих двух операторах, в которых объявляются переменные р и q: int* р; int q; i Справедливо ли данное правило для С#? Ответ. Нет. В C# действие оператора * распространяется на весь список С переменных, а оператор int* р; q; создает две переменные, которые становятся указателями. Следовательно, этот опе- ратор равносилен двум операторам int* р; int* q; Это важное отличие, которое необходимо учитывать при переносе кода C++ в С#. Ключевое слово unsafe Любой код, в котором используются указатели, должен быть отмечен как небезопас- ный с помощью ключевого слова unsafe. Отмечать можно как отдельный оператор, так и весь метод. Например, ниже приведена программа, в которой используется указатель внутри метода Мад.п(), полностью объявленного небезопасным. (Для
Небезопасный код 467 компилирования программы с небезопасным кодом нужно в командной строке цвести следующую команду: esc /unsafe filename, где filename — имя компи- лируемого файла.) ,• В программе демонстрируется использование указателя и ключевого слова unsafe. ,s-ng Sys tern ; ass UnsafeCode { Объявление метода Main как небезопасного. unsafe public static void Main() { --------------Метод Main () отмечен как небезопасный, a.nt count = 99; поскольку в нем используется указатель, int *р; // Создание указателя, имеющего тип int. р - &count; //Указателю р присваивается адрес переменной count- Console.WriteLine("Первоначальное значение переменной count = " + *р); *р = 10; // Переменной count присваивается значение с помощью указателя р. Console.WriteLine("Новое значение переменной count = " + *р); Результат ыполнения программы будет следующим: Первоначальное значение переменной count = 99 Новое значение переменной count = 10 Ключевое слово fixed Использование модификатора fixed предотвращает возможность перемещения управляемого объекта при «сборке мусора» (естественно, сам объект не перемещается, просто при оптимизации памяти для него выделяется другая область, имеющая другой адрес). Этот модификатор необходимо указывать, если на такой объект ссылается указатель. Поскольку указателю ничего не известно о действиях системы «сборки мусора», в случае перемещения объекта он будет ссылаться уже на другой объект. Ниже приведен общий синтаксис использования ключевого слова fixed. fixed (суре*р - ifixcdObj) i // Использование фиксированного объекта, для которого // указан модификатор fixed. Здесь переменная р является указателем, которому присваивается адрес объекта. Объект будет сохраняться в области памяти, выделенной для него на время выпол- нения блока кода. Модификатор fixed можно указывать и для одиночного операто- ра. но в любом случае он может использоваться только в небезопасном коде. Рассмотрим пример использования модификатора fixed. В программе демонстрируется использование небезопасного кода. sing System; class Test { public int num; public Test(int i) num ~ i; }
468 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка class UnsafeCode { // Объявление метода Main unsafe public static void Test о new Test(19); как небезопасного. Main() { fixed (int *р - &o.num Ислользонанис модификатора fixed для фиксироП вания местоположения объекта о в памяти. { // Использование модификатора fixed для присвоения указателю р адреса переменной о.num. 1 Console.WriteLine (’’Первоначальное значение переменной о.num = " + *р) ; J X *р ==• 10; // Переменной о.num присваивается значение с помощью ( // указателя р. * Console.WriteLine("Новое значение переменной о.num ~ " + *р); J } 1 } 1 t ч Результат выполнения этой программы следующий: 5 Первоначальное значение переменной о.num -19 1 Новое значение переменной о.num =10 | В программе с помощью модификатора fixed предотвращается перемещение В J памяти объекта о. Поскольку указатель р ссылается на переменную о.тип, при.'; перемещении объекта о адрес в указателе будет неверным. 5 Мы кратко описали создание и использование небезопасного кода. Если в своих ' программах вы собираетесь создавать небезопасный код, то должны изучить все эти^ вопросы более подробно. ’ Минутный практикум 1. Что такое указатель? 2. Когда используется ключевое слово unsafe? 3. Какие функции выполняет модификатор fixed? Идентификация типа во время работы программы В C# существует возможность определить тип объекта во время выполнения про- граммы. Фактически в C# есть три ключевых слова для поддержки идентификации типа во время выполнения программы, is, as и typeof- Хотя эти ключевые слова используются только при создании высокопрофессиональных программ, важно иметь о них общее представление. 1. Указатель — это переменная, в которой хранится адрес области памяти, выделенной для Другого объекта. 2. Любой код, в котором используются указатели, должен быть отмечен как небезопасный С помощью ключевого слова unsafe. 3. Использование модификатора fixed предотвращает перемещение объекта во время «сборки мусора».
Идентификация типа во время работы программы 469 Проверка типа с помощью ключевого слова is Принадлежность объекта к определенному типу можно проверить с помошью опе- ратора is, синтаксис которого выглядит следующим образом: expr IS type Здесь элемент ехрг — это выражение, тип которого сравнивается с типом, указанным справа от оператора is. Если эти типы совпадают или совместимы, то результат операции принимает значение true. В противном случае — значение false. Ниже приведена программа, в которой используется оператор is. /.< В программе демонстрируется использование оператора is. System; claso А {} class В : А [} Объект а нс является объектом типа в, следовательно, результат выполнения данного оператора is принимает значение false. public static void Main О { A a = new A(); Результат этой операции принимает значение true, поскольку объект а имеет тип А. if(a is A) Console.WriteLine("Объект а имеет тип А."); if(b is А) Console.WriteLine("Объект Ь имеет тип А, \п" + " поскольку класс В является наследующим по отношению к классу А."); if (a is В) < —-------------------------------————---------------------* Console.WriteLine("Этот оператор не будет выполнен, поскольку класс А" + " не наследует класс В. ") ; if(b is B) Console.WriteLine("Объект в имеет тип В."}; if(a is object) Console.WriteLine("Объект а имеет тип Object."); } В результате работы программы будет выведена следующая информация: Осьект а имеет тип А. осъект b имеет тип А, нискольку класс В является наследующим по отношению к классу А. -Т’ьект в имеет тип В. Осьект а имеет тип Object. большинство выражений с ключевым словом is не нуждаются в пояснении, но два аз них рассмотрим подробнее. Во-первых, обратите внимание на оператор (о is А) Console.WriteLine("Объект b имеет тип А,\п" + " поскольку класс В является наследующим по отношению к классу А."); Выражение (b is А) принимает значение true, поскольку объект Ь является объектом типа в, который наследует класс А. Следовательно, тип объекта Ь совместим с типом А. Однако класс А не наследует класс в, следовательно, при выполнении оператора •' (a is В) Console.WriteLine("Этот оператор не будет выполнен, поскольку класс А” + " не наследует класс В."); Тловное выражение оператора if принимает значение false, поскольку объект а имеет тип А, который не наследует класс в. Следовательно, эти типы несовместимы.
470 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C# Ключевое слово as Л В C# существует возможность выполнить операцию приведения типа во время ’Я выполнения программы, причем, если преобразование невозможно, это не приведет ж к возникновению исключительной ситуации. Для выполнения можно использовать £ оператор as, применение которого имеет следующий общий синтаксис: Л expr as Суре С: Здесь элемент ехрг — это выражение, которое приводится к типу, указанному справа' j от оператора as. Если операция приведения типа прошла успешно, возвращается I ссылка на тип. В противном случае возвращается пустая ссылка. * Ключевое слово typeof J Указав оператору typeof в качестве параметра тип некоторого объекта (или обычный ' тип), можно получить объект класса System.Туре. Этот объект будет содержать t информацию, ассоциированную с указываемым типом. Оператор имеет следующую гЬ общую форму синтаксиса: typeof(type); / Здесь элемент type — это тип, о котором нужно получить полную информацию, у. Класс System. Туре предназначен для получения полной информации о каком-либо типе. Например, при выполнении приведенной ниже программы выводится полное -Т имя класса StreamReader: // В программе демонстрируется использование оператора typeof. using System; 1 using System.IO; 1 2 3 4 class UseTypeof < public static void Main,) ( Type t = typeof(StreamReader); Console.WriteLine(t.FullName); } } Результат выполнения программы показан ниже. System.IO.StreamReader ® Минутный практикум 1. При помощи какого оператора можно определить, совместим ли тип объекта J с указываемым типом? 2. В чем различие между оператором as и операцией приведения типов? 3. Что оператор typeof получает в качестве параметра? 1. Совместимость типа объекта с указываемым типом можно определить при помощи опера- тора is. 2. Неудачное выполнение операции приведения типа приводит к возникновению исключи- тельной ситуации. При неудачном выполнении оператора as возвращается значение null- 3. Оператор typeof получает.в качестве параметра тип объекта или обычной переменной, о котором нужно получить полную информацию.
другие ключевые слова другие ключевые слова Завершая книгу, мы дадим краткое описание некоторых ключевых слов, о кото до лого не рассказывали. Модификатор доступа internal В дополнение к модификаторам public, private и protected, которые исполь вались в программах данной книги и с помощью которых указывалась возможно доступа к членам класса, в Cft также определен модификатор internal. Э модификатор объявляет, что член класса известен всем файлам в пределах сбор но неизвестен за пределами сборки. Сборка — это файл (или файлы), в котор содержатся компоненты программы и информация о ее версии. Следователь! можно сказать, что член класса, указанный как internal, становится известным вс программе, но нигде больше. Ключевое слово sizeof Иногда может потребоваться информация о размере в байтах (количестве байте выделенных для представления данных) одного из обычных типов С#. Для получен! такой информации используется оператор sizeof. Он имеет следующий синтакси sizeof(type) Здесь элемент type — это тип, размер которого определяется. Оператор sizec может использоваться только в небезопасном коде. Следовательно, он предназначе для применения в особых ситуациях, в частности при одновременной работе управляемым и неуправляемым кодом. Ключевое слово lock Ключевое слово lock используется при работе с множеством потоков. В С) программа может иметь два и более потоков выполнения. Когда это происходит, часп программы работают в многозадачном режиме. Следовательно, они выполняютез независимо и одновременно. При этом может возникнуть ситуация, когда два поток; попытаются одновременно использовать ресурс, которым в конкретный момент может пользоваться только один поток. Для решения этой проблемы можно создать окне критического кода, который будет выполняться в конкретный момент только одним потоком. Этот блок создается с помощью ключевого слова lock. Общая форма do синтаксиса выглядит так: -ck(obj) ( /' Блок критического кода. Здесь элемент obj — это объект, который стремится получить доступ к коду, опреде- ленному в блоке критического кода. Если один поток уже выполняет данный блок кода, 'Дорой поток будет ожидать выполнения первого потока. Только после того, как первый ноток закончит выполнение этого блока, доступ к нему получит второй поток. Поле readonly В классе можно создать поле, предназначенное только для чтения, объявив его как readonly. Поле, объявленное с указанием ключевого слова readonly, может быть
472 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C# инициализировано, но после этого его невозможно изменить. Следовательно, объ- явление поля как readonly — это хороший способ создания констант (например* для значений размерностей массива, используемых во всей программе). Поля re- adonly могут быть созданы как с модификатором static, так и без него. // В программе демонстрируется использование ключевого слова readonly. using System; . class MyClass { ------------------------— public static readonly int SIZE - 10; <-------[Фактически это обьяилснис константы] } class DemoReadOnly { public static void MainO { int[]nums - new int[MyClass.SIZE]; for(int i=0; i < MyClass.SIZE; i++) nums[i] - i; foreach(int i in nums) ) Console .Write (i + ” ”) ; // MyClass.SIZE - 100; // Ошибка!!! // Значение константы не может быть изменено. } } В начале программы при объявлении константы MyClass.size ей присваивается значение 10. После этого ее можно использовать, но нельзя изменять. Вы можете убедиться в этом, если удалите символ комментария перед началом последнего оператора и попробуете компилировать программу. Компилятор сообщит об ошибке; Ключевое слово stackalloc Выделить память из стековой памяти можно с помощью ключевого слова stackal- loc. Его можно указывать только во время инициализации локальных переменных. Общая форма синтаксиса ключевого слова stackalloc представлена ниже. type *р = stackalloc type[size] Здесь элемент р — это указатель, который получает адрес области памяти, достаточно большой для хранения указанного количества объектов типа type (количество объектов указывается вместо слова size). Ключевое слово stackalloc используется в небезопасном коде. Обычно память для объектов выделяется из кучи (динамически распределяемой области памяти), то сеть из области свободной памяти. Выделение памяти из стековой памяти является исключением. Переменные, помешенные в стековую память, не перемешаются при «сборке мусора», но существуют только во время выполнения блока, в котором они объявлены. Единственным преимуществом использования ключевого слова stackalloc является то, что после выделения памяти подобным способом программисту не нужно беспокоиться о перемещении переменных во время «сборки мусора».
Ч го делать дальше 473 Оператор using Ключевое слово using может использоваться не только в качестве директивы, но и и качестве оператора. В этом случае его синтаксис выглядит так: using (obj) { t/ Использование объекта. Здесь элемент obj — это объект, который используется внутри блока using. После выполнения операторов блока вызывается метод Dispose (). Оператор using при- меняется только для объектов, которые реализуют интерфейс System . iDisposable. Модификаторы const и volatile Модификатор const используется для объявления переменных, значение которых нельзя изменить. Этим переменным необходимо присваивать начальные значения при их объявлении. Переменная с модификатором const является константой. Оператор ce.-.st int i 10; создаст переменную i с модификатором const, имеющую значение 10. С помощью модификатора volatile компилятору сообщается, что значение переменной может изменяться способами, не определенными в программе явно. Например, значение переменной, в которой хранится информация о текущем системном времени, может обновляться автоматически операционной системой. В таком случае содержимое переменной изменяется без явного выполнения оператора присваивания. Причиной, по которой следует учитывать возможность внешнего изменения переменной, яв- ляется то, что С#-компилятору разрешено оптимизировать некоторые выражения автоматически, предполагая, что содержимое переменной не изменяется, если она не встречается в коде слева от оператора присваивания. Но если процессы за пределами выполняемого кода (например, второй поток выполнения программы) могут изменить значение переменной, то такое предположение, на основе которого компилятором выполняется оптимизация, станет ошибочным. С помощью модифи- катора volatile компилятору сообщается, что он должен получать (считывать) значение этой переменной при каждом обращении к ней. Что делать дальше Примите наши поздравления! Если вы прочитали и проработали все предыдущие 12 глав, то смело можете называть себя программистом на С#. Безусловно, многое ешс предстоит изучить — библиотеки, подсистемы, но вы имеете базу, используя которую можете расширять свои знания и опыт. Ниже мы перечислим некоторые темы, которые рекомендуем вам изучить в будущем. Использование C# для создания Windows-приложений, в частности GUI (графи- ческие пользовательские интерфейсы), в которых используются различные эле- менты GUI, такие как кнопки, меню, списки и полосы прокрутки. Создание многопотоковых приложений. Q Встроенные компоненты. 'Э Использование коллекций классов С#, поддерживающих стеки, очереди и списки. Q Использование классов, предназначенных для работы в сети. Q Управление версиями ваших приложений.
474 Глава 12. Делегаты, события, пространства имен и дополнительные элементы языка C# Контрольные вопросы 1. Напишите, как объявить делегат с именем filter, который возвращает значение типа double и получает один аргумент типа nt. 2. Как создастся многоадресный делегат? Какие существуют ограничения? 3. Какая взаимосвязь существует между делегатами и событиями? 4, Может ли событие быть широковещательным? Экземпляру или классу передается событие? 5. Назовите основное преимущество использования пространства имен. 6. Напишите синтаксис создания псевдонима с помощью оператора i.s-- ing. 7. Какие два вида операторов преобразования существуют в С#? Напишите общую форму их синтаксиса. 8. Назовите директивы препроцессора, которые используются для услов- ного компилирования. 9. Напишите синтаксис использования атрибута. 10. Что такое небезопасный код? 11. Как во время выполнения программы определить тип объекта? Продолжайте самостоятельно экспериментировать с элементами языка С#.
Герберт Шилдт С#: учебный курс Перевод с английского иод редакцией А. Падалки Редактор Художник Корректор Технический редактор Е. Насырова Н. Биржаков Е. Курбатова 3. Лобач ББК 32.973-018я7 УДК 681.3.06(075) Шилдт Г. U57 С#: учебный курс. — СПб.: Питер; К.: Издательская группа BHV, 2003. — 512 с.: ил. ISBN 966-552-121-7 ISBN 5-94723-167-0 Эта книга послужит прекрасным пособием как для специалистов, желающих изучить вык C# — новый язык фирмы Microsoft, так и для начинающих пользователей, не владеющих in одним из языков программирования. Книга написана простым и доступным языком. Ее ав- тор, Герберт Шилдт. получил мировую известность благодаря изданию серии книг по про- граммированию . □riginal English language Edition Copyright © McGraw-Hill, 2001 © Издательская группа BHV, Киев, 2003 © ЗАО Издательский дом «Питер», 2003 Права на издание получены по соглашению с McGraw-Hill. Все права защищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было фор- ме без письменного разрешения владельцев авторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издательством как на- дежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несет ответственности за возмож- ные ошибки, связанные с использованием книги. ISBN 966-552-121-7 ISBN 5-94723-167-0 ISBN 0-07-213329-5 (англ.) ООО «Питер Принт». I96I05, Санкт-Петербург, ул. Благодатная, д. 67. Лицензия ИД № 05784 от 07.09.01. ООО «Издательская группа BHV» Свидетельство о занесении в Государственный реестр серия ДК № 175 от 13.09 2000 Налоговая льгота общероссийский классификатор продукции ОК 005-93, том 2; 953005 литература учебная. Подписано в печать 31.01.03. Формат 70xi00’/16. Усл. п. л. 41.28. Доп. тираж 4000 экз. Заказ № 2281. Отпечатано с фотоформ в ФГУП «Печатный двор» им. А. М. Горького Министерства РФ по делам печати, телерадиовещания и средств массовых коммуникации. I97J10, Санкт-Петербург, Чкаловский пр., ]5.