Содержание
Введение
Кому эта книга не подойдет?
Мы знаем, о чем вы думаете
Метапознание: наука о мышлении
Вот что сделали МЫ
Что можете сделать ВЫ, чтобы заставить свой мозг повиноваться
Информация
Научные редакторы
Благодарности
И наконец…
1. Начало работы с С#: Быстро сделать что-то классное!
Visual Studio — инструмент для написания кода и изучения C#
Создание вашего первого проекта в Visual Studio
Давайте построим игру!
Как построить игру
Создание проекта WPF в Visual Studio
Построение окна с использованием XAML
Построение окна для игры
Определение размера окна и текста заголовка в свойствах XAML
Добавление строк и столбцов в сетку XAML
Выравнивание размеров строк и столбцов
Размещение элементов TextBlock в сетке
Теперь можно переходить к написанию кода игры
Генерирование метода для настройки игры
Завершение метода SetUpGame
Запуск программы
Добавление нового проекта в систему управления версиями
Следующий шаг построения игры — обработка щелчков
Реакция TextBlock на щелчки
Добавление кода TextBlock_MouseDown
Вызов обработчика события MouseDown остальными элементами TextBlock
Добавление таймера
Добавление таймера в код игры
Диагностика ошибок в отладчике
Добавьте оставшийся код и завершите построение игры
Обновление кода в системе управления версиями
Еще лучше, если…
2. Погружение в С#: Команды, классы и код
Команды являются структурными элементами приложений
Переменные используются в программах для работы с данными
Генерирование нового метода для работы с переменными
Добавление кода с использованием операторов
Использование отладчика для наблюдения за изменением переменных
Использование операторов для работы с переменными
Принятие решений в командах if
Циклы выполняют некоторые действия снова и снова
Используйте фрагменты кода для написания циклов
Элементы управления определяют механики ваших пользовательских интерфейсов
Создание приложения WPF для экспериментов с элементами управления
Добавление элемента TextBox в приложение
Добавление кода C# для обновления TextBlock
Добавление обработчика события, который разрешает вводить только числовые данные
Добавление ползунков в нижнюю строку сетки
Добавление кода С#, обеспечивающего работу элементов управления
Лабораторный курс Unity № 1: Исследование C# с Unity
Загрузка Unity Hub
Использование Unity Hub для создания нового проекта
Управление макетом Unity
Сцена как ЗD-среда
Игры Unity состоят из объектов GameObject
Использование инструмента Move для перемещения объектов GameObject
В окне Inspector выводятся компоненты GameObject
Добавление материала к объекту GameObject
Вращение сферы
Проявите фантазию!
3. Ориентируемся на объекты: Написание осмысленного кода
Некоторые методы получают параметры и возвращают значение
Программа для выбора карт
Создание консольного приложения PickRandomCards
Завершение метода PickSomeCards
Готовый класс CardPicker
Анна работает над следующей игрой
Игра Анны развивается…
Построение бумажного прототипа для классической игры
Следующий шаг: построение WPF-версии приложения для выбора карт
StackPanel — контейнер для наложения элементов
Повторное использование класса CardPicker в новом приложении WPF
Использование Grid и StackPanel для формирования макета главного окна
Формирование макета окна приложения Card Picker
Прототипы Анны выглядят замечательно…
Анна может воспользоваться объектами для решения своей задачи
Класс используется для построения объектов
Новый объект, созданный на базе класса, называется экземпляром этого класса
Экземпляры хранят данные в полях
Куча
Что на уме у вашей программы
Иногда код плохо читается
Использование содержательных имен классов и методов
Классы, парни и деньги
Простой способ инициализации объектов в C#
Используйте интерактивное окно C# для выполнения кода C#
4. Типы и ссылки: Данные и ссылки
На листах персонажей хранятся разные виды данных
Тип переменной определяет, какие данные в ней могут храниться
В C# существует несколько типов для хранения целых чисел
Поговорим о строках
Литерал — значение, записанное непосредственно в вашем коде
Переменные как емкости для данных
Другие типы тоже могут иметь разные размеры
10 литров в 5-литровой банке
Приведение типов позволяет копировать значения, которые C# не может автоматически преобразовать к другому типу
C# выполняет некоторые преобразования автоматически
При вызове метода аргументы должны быть совместимы с типами параметров
Оуэн постоянно старается улучшить свою игру…
Поможем Оуэну в экспериментах с характеристиками
Использование компилятора C# для поиска проблемной строки кода
Использование ссылочных переменных для обращения к объектам
Ссылки напоминают наклейки на ваших объектах
Если ни одной ссылки не осталось, объект уничтожается сборщиком мусора
Множественные ссылки и их побочные эффекты
Две ссылки — ДВЕ переменные, по которым можно изменять данные одного объекта
Объекты используют ссылки для взаимодействия друг с другом
Массивы содержат группы значений
Массивы могут содержать ссылочные переменные
null означает, что ссылка не указывает ни на что
Тест-драйв со случайными числами
Добро пожаловать в забегаловку эконом-класса «У неторопливого Джо»!
Лабораторный курс Unity № 2: Написание кода C# для Unity
Добавление сценария C# к объекту GameObject
Написание кода C# для поворота сферы
Добавление точки прерывания и отладка игры
Использование отладчика для понимания Time.deltaTime
Добавление цилиндра для обозначения оси Y
Добавление полей для угла поворота и скорости
Debug.DrawRay и ЗD-векторы
Запуск игры для отображения луча в представлении Scene
Поворот шара вокруг точки сцены
Эксперименты с поворотами и векторами в Unity
Проявите фантазию!
5. Инкапсуляция: Умейте хранить секреты
Создание консольного приложения для вычисления повреждений
Разработка XAML для WPF-версии калькулятора повреждений
Код программной части для WPF-калькулятора повреждений
Попробуем исправить ошибку
Использование Debug.WriteLine для вывода диагностической информации
Возможность некорректного использования объектов
Инкапсуляция подразумевает ограничение доступа к части данных класса
Применение инкапсуляции для управления доступом к методам и полям класса
Но ДЕЙСТВИТЕЛЬНО ЛИ поле RealName надежно защищено?
К приватным полям и методам могут обращаться только экземпляры того же класса
Для чего нужна инкапсуляция? Представьте объект в виде «черного ящика»…
Воспользуемся инкапсуляцией для улучшения класса SwordDamage
Инкапсуляция обеспечивает безопасность данных
Консольное приложение для тестирования класса PaintballGun
Свойства упрощают инкапсуляцию
Изменение метода Main для использования свойства Balls
Автоматически реализуемые свойства упрощают ваш код
Использование приватного set-метода для создания свойств, доступных только для чтения
А если потребуется изменить размер магазина?
Использование конструктора с параметрами для инициализации свойств
Передача аргументов при использовании ключевого слова \
6. Наследование: Генеалогическое древо объектов
Команды switch для выбора из нескольких кандидатов
И еще… Можно ли вычислять повреждения от кинжала? От булавы? И шеста? И…
Если в ваших классах используется наследование, код достаточно написать только один раз
Постройте модель классов: начните с общего и переходите к конкретике
Как бы вы спроектировали симулятор зоопарка?
У разных животных разное поведение
Каждый субкласс расширяет свой базовый класс
Расширение базового класса
Субкласс может переопределять методы для изменения или замены унаследованных компонентов
Некоторые компоненты реализованы только в субклассе
Анализ переопределения в отладчике
Построение приложения для изучения virtual и override
Субкласс может скрывать методы базового класса
Использование ключевых слов override и virtual для наследования поведения
Если базовый класс содержит конструктор, ваш субкласс должен его вызвать
Субкласс и базовый класс могут иметь разные конструкторы
Пора доделать приложение для Оуэна
Построение системы управления ульем
Модель классов системы управления ульем
Класс Queen: как матка управляет рабочими
Пользовательский интерфейс: добавление кода XAML главного окна
Обратная связь направляет работу системы управления ульем
Система управления ульем работает в пошаговом режиме… Преобразуем ее для работы в реальном времени
Экземпляры некоторых классов никогда не должны создаваться
Абстрактный класс — намеренно незавершенный класс
У абстрактных методов нет тела
Абстрактные свойства работают как абстрактные методы
Смертельный ромб
Лабораторный курс Unity № 3: Экземпляры GameObject
Создайте новый материал в папке Materials
Создание бильярдного шара в случайной точке сцены
Применение отладчика для понимания Random.value
Преобразование объекта GameObject в заготовку
Создание сценария для управления игрой
Присоединение сценария к главной камере
Запустите свой код кнопкой Play
Работа с экземплярами GameObject в окне Inspector
Предотвращение перекрытия шаров
Проявите фантазию!
7. Интерфейсы, приведение типов и is: Классы должны держать обещания
Мы можем воспользоваться приведением типов для вызова метода DefendHive…
Интерфейс определяет методы и свойства, которые должны быть реализованы классом…
Потренируемся в использовании интерфейсов
Создать экземпляр интерфейса невозможно, но можно получить ссылку на интерфейс
Ссылки на интерфейсы являются обычными ссылками на объекты
RoboBee 4000 может выполнять работу пчел без расхода драгоценного меда
Свойство Job в интерфейсе IWorker — костыль
Использование is для проверки типа объекта
Использование is для обращения к методам субкласса
А если мы захотим, чтобы другие животные плавали или охотились в стае?
Использование интерфейсов для работы с классами, выполняющими одну задачу
В C# также существует другой инструмент для безопасного преобразования типов: ключевое слово as
Пример повышающего приведения типа
Повышающее приведение преобразует CoffeeMaker в Appliance
Повышающие и понижающие приведения типов также работают и с интерфейсами
Интерфейсы могут наследовать от других интерфейсов
Интерфейсы могут содержать статические компоненты
Реализации по умолчанию определяют тело методов интерфейса
Добавление метода ScareAdults с реализацией по умолчанию
Связывание данных обеспечивает автоматическое обновление элементов WPF
Связывание данных в системе управления ульем
«Полиморфизм» означает, что один объект может существовать в разных формах
8. Перечисления и коллекции: Организация данных
Перечисления предназначены для работы с наборами допустимых значений
Для создания колоды карт можно воспользоваться массивом…
С массивами бывает неудобно работать
В списках можно хранить коллекции… чего угодно
Списки обладают большей гибкостью, чем массивы
Построим приложение для хранения обуви
В обобщенных коллекциях могут храниться любые типы
Инициализаторы коллекций похожи на инициализаторы объектов
Создание списка уток
Списки удобны, но с сортировкой могут возникнуть проблемы
IComparable<Duck> помогает списку List сортировать объекты Duck
Использование IComparer для определения порядка сортировки
Создание экземпляра компаратора
Компараторы могут выполнять сложные сравнения
Переопределение метода ToString позволяет объекту описать себя
Обновите циклы foreach, чтобы объекты Duck и Card выводили свои описания на консоль
Использование Dictionary для хранения ключей и значений
Краткая сводка функциональности Dictionary
Построение программы с использованием словаря
Другие разновидности коллекций…
Очередь работает по принципу FIFO — «первым вошел, первым вышел»
Стек работает по принципу LIFO — «последним вошел, первым вышел»
Упражнение: две колоды
Лабораторный курс Unity № 4: Пользовательские интерфейсы
Включение двух режимов в игру
Добавление игрового режима
Добавление пользовательского интерфейса к игре
Настройка объекта Text для вывода счета в UI
Кнопка для вызова метода, запускающего игру
Кнопка Play Again и текущий счет
Завершение кода игры
Проявите фантазию!
9. LINQ и лямбда-выражения: Контроль над данными
Использование LINQ для управления коллекциями
LINQ работает с любыми реализациями IEnumerable<T>
Синтаксис запросов LINQ
LINQ работает с объектами
Использование запроса LINQ в приложении для Джимми
Ключевое слово var позволяет C# определить тип переменной за вас
Запросы LINQ выполняются только при обращении к результатам
Использование запросов group для разделения последовательности на группы
Использование запросов join для слияния данных из двух последовательностей
Использование ключевого слова new для создания анонимных типов
Модульные тесты помогают понять, как работает код
Добавление проекта модульного теста в приложение Джимми
Первый модульный тест
Написание модульного теста для метода GetReviews
Написание модульных тестов для обработки граничных случаев и аномальных данных
Оператор => и создание лямбда-выражений
Тест-драйв лямбда-выражений
Рефакторинг клоунов с использованием лямбда-выражений
Использование оператора ?: для принятия решений в лямбда-выражениях
Лямбда-выражения и LINQ
Запросы LINQ могут записываться в виде сцепленных вызовов методов LINQ
Использование оператора => для создания выражений switch
Исследование класса Enumerable
Ручное создание последовательности с поддержкой перебора
Упражнение: Go Fish
10. Чтение и запись файлов: Прибереги последний байт для меня
Различные потоки для разных данных
Объект FileStream читает и записывает байты в файл
Запись текста в файл за три простых шага
Дьявольский план Пройдохи
Использование StreamReader для чтения файла
Данные могут проходить через несколько потоков
Работа с файлами и каталогами с использованием статических классов File и Directory
Интерфейс IDisposable обеспечивает корректное закрытие объектов 585 Предотвращение ошибок файловой системы командами using
Потоки MemoryStream и хранение данных в памяти
При сериализации объекта также сериализуются все объекты, на которые он ссылается…
Использование JsonSerialization для сериализации объектов
JSON включает только данные, но не конкретные типы C#
Следующий шаг: углубленный анализ данных
Строки C# кодируются в Юникоде
Поддержка Юникода в Visual Studio
.NET использует Юникод для хранения символов и текста
C# может использовать массивы байтов для перемещения данных 608 Использование BinaryWriter для записи двоичных данных
Использование BinaryReader для чтения данных
Дамп позволяет просматривать байты в файлах
Использование StreamReader для вывода шестнадцатеричного дампа 613 Использование Stream.Read для чтения байтов из потока
Аргументы командной строки
Упражнение: Hide and Seek
Лабораторный курс Unity № 5: Отслеживание лучей
Настройка камеры
Создание объекта GameObject для игрока
Знакомство с системой навигации Unity
Создание сетки NavMesh
Автоматическая навигация в игровой области
11. Капитан Великолепный: Смерть объекта
Когда именно выполняется финализатор?
Финализаторы не могут зависеть от других объектов
Структура похожа на объект…
…но не является объектом
Значения копируются, ссылки присваиваются
Структуры относятся к типам значений; объекты относятся к ссылочным типам
Стек и куча: подробнее о памяти
Параметры out и возвращение нескольких значений методом
Передача по ссылке с модификатором ref
Необязательные параметры и значения по умолчанию
Ссылка null не указывает ни на какой объект
Ссылочные типы, не допускающие null, помогут избежать NRE
Оператор объединения с null ??
Безопасная работа с типами значений, допускающими null
Капитан… уже не такой Великолепный
Методы расширения добавляют новое поведение в существующие классы
Расширение фундаментального типа: string
12. Обработка исключений: Борьба с огнем надоедает
Когда ваша программа выдает исключение, CLR генерирует объект Exception
Все объекты Exception наследуют от System.Exception
Для некоторых файлов вывод дампа невозможен
Что происходит при вызове небезопасного метода?
Обработка исключений с try и catch
Отслеживание передачи управления в try/catch
Код блока finally выполняется всегда
Перехват всех исключений
Использование исключения, подходящего для конкретной ситуации
Фильтры исключений повышают точность обработки исключений
Наихудший блок catch: универсальный перехват с комментариями
Лабораторный курс Unity № 6: Перемещение по сцене
Добавление платформы в сцену
Изменение настроек предварительного построения
Включение лестницы и наклонной плоскости в NavMesh
Решение проблем с высотой в NavMesh
Добавление препятствия в сетку NavMesh
Добавление сценария для перемещения препятствия вверх и вниз
Проявите фантазию!
Создание вашего первого проекта в Visual Studio for Mac
Давайте построим игру!
Как построить игру
Создание проекта Blazor WebAssembly в Visual Studio
Запуск веб-приложения Blazor в браузере
Visual Studio помогает в написании кода C#
Завершение создания списка эмодзи и вывод их в приложении
Перестановка животных в случайном порядке
Добавление нового проекта в систему управления версиями
Добавление кода C# для обработки щелчков
Назначение обработчиков щелчков кнопкам
Тестирование обработчика события
Диагностика проблемы в отладчике
Отладка обработчика события
Поиск ошибки, породившей проблему…
Добавление кода для сброса игры при победе
Добавление таймера
Добавление таймера в код игры
Очистка меню навигации
Создание нового проекта Blazor WebAssembly App
Создание страницы с ползунком
Добавление текстового поля
Добавление селекторов для выбора цвета и даты
Построение Blazor-версии приложения для выбора карт
Страница состоит из строк и столбцов
Ползунок использует связывание данных для обновления переменной
Добро пожаловать в забегаловку эконом-класса «У неторопливого Джо»!
II. Ката программирования: Ката программирования для опытных и/или нетерпеливых
Текст
                    Отзывы о книге
«Огромное спасибо!Ваши книги помоглимненачать карьеру».
— РайанУайт,разработчик игр
«Если вы являетесь начинающим разработчикомC#(добропожаловатьнаборт!),я настоятельно реко­
мендую вам эту книгу. Эндрю иДженнифер написали лаконичное, авторитетное, а самое главное — ув­
лекательноевведение вразработкуC#. Жаль,чтоу менянебыло этойкниги,когда я только изучалC#».
— ДжонГаллоуэй, старшийруководитель проектов группы.NET CommunityTeam
в компании Microsoft
«Мало того что вкниге изложены всенюансы, вкоторыхяразобрался далеко не сразу, — в нейприсут­
ствует та самаямагия серииHeadFirst,благодарякоторой она таклегко читается».
— ДжеффКунц, старшийразработчикC#
«“HeadFirst. ИзучаемC#“ — замечательная книга с занимательными примерами,благодаря которымобу­
чениестановится интересным».
— ЛиндсиБьеда, ведущий разработчик
«“HeadFirst. ИзучаемC#“ отлично подойдет как для начинающихразработчиков, так иразработчиков
сопытом работынаJava(такихкакя.)Авторынеделают никакихдопущений относительноквалификации
читателя,но уровеньизложениярастет достаточнобыстродлятех, укогоуже есть опыт программирова­
ния, — выдержать такойбаланс непросто. Книгапомогла мне оченьбыстро выйтина требуемыйуровень
вмоем первом крупномасштабном проектеразработкиC# на работе— я настоятельно рекомендую ее».
— Шалева Одусанья, менеджер
«“HeadFirst. Изучаем C#“ — превосходный, простой и интересный способ изучения C#. Это лучшая
книга для новичков C#, которуюякогда­либо видел, — примеры понятны, материал излагаетсякратко
ихорошо написан. Мини­игрыпомогут закрепить новыезнания ввашеммозгу.Превосходная книга для
тех,кто предпочитаетучитьсяна деле».
— ДжонниХалиф, cовладелецSOUTHWORKS
«“HeadFirst. ИзучаемC#“ — подробноеруководство по изучению C#, которое читается как дружеская
беседа.Многочисленныеупражненияпо программированию делают ееболее интересной даже при из­
ложениисамыхнепростых концепций».
— РебеккаДанн-Кран, соучредитель SemaphoreSolutions
«Яникогда не читал компьютерные книги откорки до корки,но эта книга удерживала мой интерес от
первой страницы до последней. Если вы хотите глубоко изучить C#, да еще и получитьудовольствие,
это ТА САМАЯ книга,котораявам нужна».
— ЭндиПаркер, начинающийпрограммистC#


Другие отзывы о Head First C# «Трудно нормально изучать язык программированиябез хороших, увлекательных примеров — в этой книге их полно! Книга поможет начинающим программистам всех сортовналадить длительныеи про­ дуктивные отношения с C# и .NET Framework». — К рисБерроуз,разработчик «КнигаЭндрюиДженни “HeadFirst. ИзучаемC#“стала превосходным учебником для изученияC#. Она очень доступна, но при этом материал излагается весьма подробно и в неповторимом стиле. Если вас отпугнулиболеетрадиционныекнигио C#, эта вам понравится». — ДжейХильярд, директори специалистпо архитектурепрограммнойбезопасности, автор книги«C#6.0Cookbook» «Ярекомендую эту книгу каждому, кто разыскивает хорошее введение в мир программирования иC#. Ссамой первой страницы авторы знакомятчитателя с некоторыми нетривиальными концепциямиC# впростой, доходчивой манере. В конценекоторыхбольшихпроектов/лабораторныхработчитатель может оглянутьсяна своипрограммы и поразиться тому, чего он достиг». — Дэвид Стерлинг, старшийразработчик «“HeadFirst. ИзучаемC#“ — весьма приятныйучебник, полный запоминающихся примеров иувлека­ тельныхупражнений.Ееживойстиль изложениянаверняка привлечетчитателей—отпримеровс юмо­ ристическимианнотациями до «Беседу камина», в однойиз которых абстрактный класс и интерфейс устраивают словесную перепалку! Длялюбого новичка в программировании просто не существует лучшего способа погрузитьсявтему». — ДжозефАлбахари, cоздатель LINQPad, соавторкниг «C#8.0inaNutshell»и «C#8.0Pocket Reference» «“HeadFirst. ИзучаемC#“хорошо читалась ибылапонятной.Ярекомендую этукнигукаждомуразработ­ чику,желающему погрузитьсявводы C#. Япорекомендую ееразработчику,которыйхочетнайтиболее эффективныйспособобъяснить, какработаетC#, своимменее просвещенным друзьям­разработчикам». — ДжузеппеТуритто, техническийдиректор «Эндрю иДженни создали еще одинвпечатляющий учебный курс изсерии “HeadFirst”. Беритекаран­ даш, садитесь за компьютер и получайтеудовольствие,напрягая свое левое полушарие мозга, правое полушариеи чувство юмора». — БиллМетельски, системныйаналитик «Чтениекнигипринесло незабываемыевпечатления. Я ещеневстречалкнижнойсерии, котораябытак хорошоучила... Определенно рекомендую эту книгувсем,кто хочет изучатьC#». — К ришнаПала,MCP
Отзывы о других книгах из серии Head First «Я получилэтукнигу вчера, началчитать ее...ине могостановиться. Безусловно, этоочень круто.Книга читается легко, но авторы излагаютбольшой объем материала, и все написано по сути. Я под впечат­ ле нием» . — ЭрикГамма, соавторкниги«Паттерныпроектирования». «Одна из самыхувлекательныхиумныхкниг по проектированию программного обеспечения,которые мне когда­либо попадались». — АаронЛаберг, старшийвице-президентпо технологиям иразработкепродуктов, ESPN «То,чтокогда­тобылодолгим учебнымпроцессомпроби ошибок,сократилось до увлекательнойкниги вмягкой обложке». — МайкДэвидсон, бывшийвице-президентпо проектированию, Twitter, основатель Newsvine «Элегантный дизайн лежит в основе каждой главы, каждая концепция передается с равными дозами прагматизма и остроумия». — КенГолдстейн, исполнительныйвице-президент и директор-распорядитель, Disney Online «Обычно когда ячитаю книгуили статью о паттернахпроектирования,мнеиногда приходится щипать себя,чтобыне отвлекаться отчтения... Но только нес этойкнигой.Какбыстранно этони прозвучало, с этойкнигой изучениепаттернов проектирования становится интересным». Иесли другиекнигиопаттернах проектированияговорят: “Расслабься... Тебетепло... Твои веки тяже­ леют...”, эта книга во все горло кричит: “Взбодрись,парень!”». — ЭрикВьюлер «Я буквально влюбленвэту книгу.Ядажепоцеловал ее на глазаху жены». — СатишКумар
A Learner’s Guide to Real-World Programming with C# and .NET Core Head First C# Andrew Stellman & Jennifer Greene Wouldn‛t it be dreamy if there was a C# book that was more fun than memorizing a dictionary? It‛s probably nothing but a fantasy... 4-th edition
A Learner’s Guide to Real-World Programming with C# and .NET Core Head First C# Andrew Stellman & Jennifer Greene Wouldn‛t it be dreamy if there was a C# book that was more fun than memorizing a dictionary? It‛s probably nothing but a fantasy... 4-th edition 4-е издание Head First Эндрю Стиллмен Дженнифер Грин Как бы было хорошо найти книгу по C#, которая будет веселее зазубривания словаря... Об этом можно только мечтать... 2022 Изучаем C#
Руководитель дивизиона Ю. Сергиенко Литературный редактор Н. Викторова КорректорС.Беляева Художественный редактор В. Мостипан ВерсткаЛ.Егорова,Е.Неволайнен ИзготовленовРоссии. Изготовитель:ООО«Прогресскнига». Местонахожденияифактическийадрес: 194044,Россия, г. Санкт-Петербург, Б.Сампсониевский пр., д. 29А, пом. 52. Тел.: +78127037373. Датаизготовления: 02.2022. Наименование: книжная продукция. Срокгодности: неограничен. ИмпортервБеларусь:ООО«ПИТЕРМ», 220020,РБ,г.Минск, ул.Тимирязева, д. 121/3,к.214,тел./факс:2088001. Налоговаяльгота — общероссийский классификатор продукции ОК034-2014,58.11.12.000 —Книги печатные профессиональные, техническиеинаучные. Подписано в печать13.01.22 .Формат84×108/16. Бумага офсетная. Усл. п . л. 80 ,640. Тираж1200. Заказ0000. Б Б К 32.973. 2- 018.1 УДК 004.43 Стиллмен Эндрю, Грин Дженнифер С80 Head First. Изучаем C#. 4-е изд. / Пер. с англ . Е. Матвеева. — СПб.: Питер, 2022. —768 с.: ил. — (Серия «HeadFirst O’Reilly»). ISBN 978- 5- 4461- 3943- 9 Серия Head Firstпозволяетсразуприступитьк созданиюсобственного коданаC#, даже если увас нет никакого опыта программирования. Не нужнотратить времянаизучение скучных спецификаций и примеров! Вы освоите необходимый минимум инструментов и сразуприступите к забавным и интересным программным проектам, от разработки 3D-игры до создания серьезного приложения иработы с данными. Четвертое издание книги было полностьюобновлено и переработано, чтобы рассказать о возможностях современных C#,Visual Studioи .NET,оно будет интересно всем,ктоизучает язык про- граммирования С#.Особенностью данногоиздания является уникальныйспособподачи материала, выделяющий серию «Head First» издательства O’Reilly в ряду множества скучныхкниг,посвященныхпрограммированию. 16+(Всоответствии сФедеральным законом от 29декабря 2010 г.No 436-ФЗ.) Права на издание полученыпо соглашениюс O’Reilly. Все правазащищены. Никакая часть данной книги не может быть воспроизведена в какой бы то ни было форме безписьменного разрешения владельцевавторских прав. Информация, содержащаяся в данной книге, получена из источников, рассматриваемых издатель- ством как надежные. Тем не менее, имея в виду возможные человеческие или технические ошибки, издательство не может гарантировать абсолютную точность и полноту приводимых сведений и не несетответственности за возможные ошибки, связанные с использованием книги. Издательство не несетответственности за доступностьматериалов, ссылки на которые вы можетенайти в этой книге. На моментподготовкикниги к изданиювсе ссылки на интернет-ресурсы былидействующими. ISBN 978-1491976708 англ. ISBN 978-5 -4461 -3943 -9 Authorized Russiantranslation of the English edition of Head First C#, 4th Edition ISBN9781491976708 ©2021JenniferGreene,AndrewStellman. Thistranslation is published and sold bypermission of O’Reilly Media, Inc., which ownsorcontrols all rights topublish and sell the same. ©Переводна русский языкОООИздательство «Питер», 2022 ©Издание нарусском языке,оформление ООО Издательство «Питер», 2022 ©Серия «Head First O’Reilly», 2022
Эта книга посвящается киту Сладжи, кото рый приплы л в Брукли н 17 апреля 2007 года Ты пробыл в нашем канале всего день, но нав сег да ос танешь ся в наших с ердц ах
Дженнифер Грин изучала в колледже фило­ софию и, какимногиеееоднокурсники, несмогла найтиработу по специальности. Но благодаря способностям кразработке программногообеспе­ чения она начала работать вонлайновойслужбе. В1998годуДженнипереехала вНью­Йоркиустро­ иласьв фирмупоразработкефинансовогоПО.Она управляла командамиразработчиков, тестировщи­ кови программистов,занимавшихсяразработкой ПОвмедийнойифинансовой областях. Затем она много путешествовала по мирус раз­ личнымикомандамиразработчиковиреализовала целыйрядзамечательныхпроектов. Дженифер обожает путешествия,индийское кино, комиксы,компьютерныеигры иогромную сибир­ скую кошку. Эндрю Стеллман родился в Нью­Йорке, но жил в Миннеаполисе, Женеве и Питтсбурге... два раза... — сначала в школе Карнеги­Меллона, а затем, когда они с Дженни основали консал­ тинговую компанию и писали первую книгу для изда тель с тва « О ’Рейли» . Первой его работой после колледжа стало про­ граммное обеспечение для фирмы звукозаписи EMI­Capitol Records, что вполне логично, ведь он учился вLaGuardia по классу виолончели и джазовойбасс­гитары. Сначала они сДженнира­ ботали в компании по производству финансового ПО на Уолл­стрит, где Эндрю руководил группой программистов. На протяжении многих лет он был вице­президентом крупного инвестиционно­ го банка, конструировал масштабныесерверные системы, управлялбольшими международными командами разработчиков ПО и консультиро­ вал фирмы, школы и организации, в том числе Microsoft, национальное бюро экономических исследованийиМассачусетский технологический институт. За это время ему удалось поработать с замечательными программистами и многому от нихнаучиться. В свободное времяЭндрю создает бесполезные (нозабавные) программы,играетв компьютерные игры, практикует тайцзицюань и айкидо и забо­ тится о своем карликовом шпице. авторы Дженнии Эндрю создают программы и пишут о разработке программного обеспечения с 1998 года. Их первая книга — Applied Software Project Management — вышла в издательстве «О’Рейли» в 2005 году. В этом же издательстве вышли Beautiful Teams (2009) и первая книга серии Head First Head First PMP (2007). В 2003 году они основали компанию Stellman & Greene Consulting, чтобы разрабатывать про- граммное обеспечение для ученых, изучающих последствия использования отравляющих веществ для ветеранов войны во Вь етнаме. Кроме программ и книг, эта компания оказывает консалтинговые услуги и выступает на конференциях и встречах разработчиков ПО, архитекторов и руководите- лей проектов. С ними можно познакомиться в блоге Building Better Software: http://www.stellman-greene.com И в Twitter @AndrewStellman и @JennyGreene Дженни Эндрю Спасибо, что купили нашу книгу! Мы писали ее с удовольствием и надеемся, что вы получите кайф от ее прочтения... ...так к ак м ы уверены, что вы проведете много часов, изучая C#. Автор этой фотографии (как и снимка канала Гованус) — Ниша Сондхе
ог л авл ение 9 Содержание (сводка) Сделаем игру чуть более азартной!В нижней части окна выводитсявремя, про- шедшее смомента запуска игры. Показания таймера постоянно увеличиваются, а останавливается таймер только посленахождения по- следней пары. Введение 29 1 Началоработы сС#: Быстро сделать что-то классное! 41 2 Погружение в C#:Команды, классы икод 89 Лабораторный курс UnityNo 1: Исследование C# сUnity 127 3 Ориентируемся наобъекты: Написание осмысленного кода 143 4 Типы иссылки: Данные иссылки 195 Лабораторный курс UnityNo 2: Написание кода C# для Unity 000 5 Инкапсуляция: Умейтехранить секреты 267 6 Наследование:Генеалогическое древо объектов 313 Лабораторный курс UnityNo 3: ЭкземплярыGameObject 383 7 Интерфейсы,приведение типов иis: Классыдолжны держать обещания 395 8 Перечисления и коллекции:Организация данных 445 Лабораторный курс UnityNo 4: Пользовательские интерфейсы 493 9 LINQи лямбда-выражения: Контроль надданными 507 10 Чтение изаписьфайлов:Приберегипоследнийбайт дляменя 569 Лабораторный курс UnityNo 5: Отслеживание лучей 617 11 Капитан Великолепный:Смерть объекта 627 12 Обработкаисключений:Борьба согнем надоедает 663 Лабораторный курс UnityNo 6: Перемещениепо сцене 691 I Проекты ASP.NETCoreBlazor:Visual Studio дляпользователейMac 703 II Катапрограммирования: Ката программирования для опытных и/или не тер пеливы х 765 Содержание (настоящее) Ваш мозг и C#.Вы учитесь — готовитесь к экзамену. Или пытаетесь освоить сложную техническую тему.Ваш мозг хочет оказать вамуслугу. Он старается сделать так, чтобы на эту очевидно несущественную информацию не тратились драгоценные ресурсы. Их лучше потратить на что-нибудь важное. Так как же заставить его изучить C#? Введение Для кого написана эта книга? 30 Кому эта книгане подойдет? 30 Мы знаем, о чем вы думаете 31 Метапознание:наукаомышлении 33 Вотчтосделали МЫ 34 Чтоможетесделать ВЫ, чтобы заставить своймозг повиноваться 35 Информация 36 Научныередакторы 38 Бла г ода рно сти 39 Инаконец... 39
оглавление 10 Зачем вам изучать C# 42 Visual Studio—инструментдля написания кода иизучения C# 43 Создание вашегопервогопроекта вVisual Studio 44 Давайте построим игру! 46 Как построить игру 47 Создание проектаWPFв VisualStudio 48 Построениеокнасиспользованием XAML 52 Построениеокнадля игры 53 Определение размера окнаи текста заголовка в свойствахXAML 54 Добавление строк истолбцов в сетку XAML 56 Выравнивание размеров строк и столбцов 57 Размещение элементов TextBlock в сетке 58 Теперь можнопереходитьк написаниюкодаигры 61 Генерированиеметодадля настройки игры 62 Завершение метода SetUpGame 64 Запускпрограммы 66 Добавление новогопроекта в систему управления версиями 70 Следующий шаг построения игры —обработкащелчков 73 Реакция TextBlock нащелчки 74 Добавление кода TextBlock_MouseDown 77 Вызов обработчика события MouseDown остальными элементами TextBlock 78 Добавление таймера 79 Добавление таймерав код игры 80 Диагностикаошибок в отладчике 82 Добавьте оставшийся код изавершите построение игры 86 Обновлениекодав системеуправления версиями 87 Еще лучше,если... 88 Быстро сделать что-то классное! 1 Хотитепрограммировать быстро? C# — это мощный язык программиро- вания. Благодаря Visual Studio вам не потребуется писать непонятный код, чтобы заставить кнопкуработать. Вместо того чтобы запоминать параметры метода для имени и для ярлык а кнопки, вы сможете создать действительно классное приложение. Звучит заманчиво? Тогда переверните страницу и приступим к делу. Начало работы с c# MainWindow.xaml.cs MainWindow.xaml Создание проекта конСтруиро- вание окна напиСание кода C# обработка щелчков добавление тайм е ра
ог л авл ение 11 Присмотримсяк файлам консольного приложения 90 Дваклассамогут находиться в одном пространствеимен (ифайле!) 92 Команды являются структурными элементами приложений 95 Переменные используются в программах дляработыс данными 96 Генерированиенового метода для работы спеременными 98 Добавление кодас использованием операторов 99 Использованиеотладчикадля наблюдения за изменением переменных 100 Использованиеоператоров для работы с переменными 102 Принятиерешений вкомандах if 103 Циклы выполняютнекоторые действия снова и снова 104 Используйтефрагменты кода для написания циклов 107 Элементы управления определяют механики ваших пользовательских интерфейсов 111 Создание приложения WPFдля экспериментов сэлементамиуправления 112 Добавление элементаTextBox в приложение 115 Добавление кодаC# для обновления TextBlock 118 Добавление обработчика события, которыйразрешает вводить толькочисловые данные 119 Добавление ползунков внижнюю строку сетки 123 Добавление кодаC#,обеспечивающего работу элементовуправления 124 Команды, классы и код Вы не просто пользовательIDE.Вы — разработчик. IDE может сделать за вас очень многое, и все же ее возможности небезграничны. Visual Studio — одна из самых совершенных систем разработки программного обеспечения, однако мощнаяIDE — только начало . Пришло время заняться углубленным изучением кода C#: какую структуру он имеет, как он работает, как управлять им... Потому что нет предела тому, что вы можете делать в ваших приложениях. Погружение в C# 2 Namespace Class Met hod 1 statement statement Method 2 statement statement
оглавление 12 Добро пожаловать на первый урок «Лабораторный курс Unity». Написание кода — навык, и, как и любой другой навык, он раз- вивается за счет практики и экспериментирования. И в этом отношении Unity может стать очень полезным инструментом. В первой лабораторной работе мы введем вас в курс дела. Вы начнете ориентироваться в редакторе Unity, а также создавать 3D-объекты и оперировать ими. Лабораторный курс Unity No 1 Исследование C# с Unity Unity —мощный инструмент для разработкиигр 128 ЗагрузкаUnity Hub 129 ИспользованиеUnity Hub для создания нового проекта 130 Управление макетом Unity 131 Сцена как 3D-среда 132 Игры Unity состоят из объектов GameObject 133 ИспользованиеинструментаMove для перемещения объектов GameObject 134 Вокне InspectorвыводятсякомпонентыGameObject 135 Добавлениематериалак объекту GameObject 136 Вращение сферы 139 Проявите фантазию! 142
ог л авл ение 13 Есликод полезен, он используетсяповторно 144 Некоторыеметоды получают параметры ивозвращаютзначение 145 Программа для выборакарт 146 Созданиеконсольногоприложения PickRandomCards 147 Завершениеметода PickSomeCards 148 Готовый класс CardPicker 150 Аннаработает над следующей игрой 153 ИграАнны развивается... 154 Построение бумажногопрототипадля классической игры 156 Следующийшаг:построение WPF-версии приложения для выборакарт 158 StackPanel — контейнер для наложения элементов 159 ПовторноеиспользованиеклассаCardPicker в новом приложении WPF 160 Использование Grid и StackPanel для формирования макета главного окна 161 Формирование макетаокна приложения Card Picker 162 Прототипы Анны выглядят замечательно... 165 Аннаможет воспользоваться объектами для решения своей задачи 166 Классиспользуется для построения объектов 167 Новый объект, созданныйнабазе класса,называется экземпляром этогокласса 168 Хорошее решениедля Анны(собъектами) 169 Экземплярыхранят данные в полях 173 Ку ча 176 Чтонаумеу вашей программы 177 Иногда кодплохочитается 178 Использование содержательных имен классов и методов 180 Классы, парни иденьги 186 Простойспособ инициализации объектов в C# 188 Используйте интерактивное окно C# для выполнения кода C# 194 3 Написание осмысленного кода Каждая написанная вами программа решает некоторую задачу. Когда вы пишете про- грамму, всегда желательно заранее подумать, какую задачу должна решать ваша программа. Вот почему объекты приносят такую пользу. Они позволяют сформировать структуру кода в соотв етств ии с решаемой задач ей, ч тобы в ы могли тратить в ремя н а задачу, н ад к оторой работаете, не отвлекаясь на механику написания кода. Если вы правильно используете объ- екты (идействительно хорошо продумалиих при проектировании), получившийся код будет интуитивно понятным, будет легко читаться иизменяться. Ориентируемся на объекты 4 OF HEARTS 2 OF DIAMONDS KING OF SPADES ACEOF HEARTS 7OF CLUBS 10OF SPADES JACK OF CLUBS 9 OF HEARTS 9 OF DIAMONDS 3 OF CLUBS ACEOF SPADES PICKSOME CARDS
ог лав лен ие 14 4 Данные и ссылки Типы и ссылки Оуэну нужна нашапомощь! 196 Налистах персонажей хранятся разные виды данных 197 Типпеременнойопределяет,какиеданныев ней могутхраниться 198 В C# существует несколькотиповдля хранения целых чисел 199 Поговорим о строках 201 Литерал —значение, записанное непосредственнов вашем коде 202 Переменные как емкости для данных 205 Другие типы тожемогут иметь разныеразмеры 206 10 литровв 5-литровойбанке 207 Приведение типов позволяет копировать значения, которые C# не может автоматическипреобразоватьк другому типу 208 C# выполняет некоторые преобразованияавтоматически 211 При вызовеметода аргументыдолжны быть совместимы стипами параметров 212 Оуэнпостоянностарается улучшить свою игру... 214 Поможем Оуэну в экспериментах схарактеристиками 216 Использование компилятораC# для поиска проблемной строки кода 218 Использование ссылочных переменных для обращения к объектам 226 Ссылкинапоминают наклейкинаваших объектах 227 Если ни одной ссылки не осталось, объект уничтожаетсясборщиком мусора 228 Множественныессылки иих побочные эффекты 230 Двессылки— ДВЕ переменные, покоторым можноизменять данные одногообъекта 237 Объекты используютссылкидля взаимодействия друг сдругом 238 Массивысодержат группы значений 240 Массивымогут содержать ссылочные переменные 241 null означает, чтоссылканеуказывает ни на что 243 Тест-драйв сослучайными числами 247 Добропожаловать взабегаловку эконом-класса «УнеторопливогоДжо»! 248 Чем были бы наши приложениябезданных?Задумайтесь на минуту.Безданных наши программы... в общем, трудно представить, что кто -то станет писать код без данных. Вы запрашиваете информацию у ваших пользователей; эта информация используется для поиска данных или генерирования новой информации, которая возвращается пользователю. Собственно, практически все, что вы делаете в программировании, требует работы с данными в той или иной форме. В этой главе вы узнаете все тонкости типов данных и ссылок C#, узнаете, как работать с данными в программах, и даже узнаете кое-что новое об объектах (представьте, объекты — тоже данные!). Charac t erName CharacterSheet Leve l A l i gnmen t Charca t erC l ass P i c t ure St reng th D exter i ty Inte l l i ge nce W i sd om C har i sma Spe l l Sav i ng T hrow Po i son Sav i ngThrow Mag i c Wand Sav i ngThrow ArrowSav i ng T hrow ELLI WYNN 7 LAWFULGOOD WIZARD 9 11 10 17 15 Создание ссылки вы- глядит так, словно вы пишете имя на наклей- ке и прикрепляете ее к объекту. Надпись ста- новится своего рода «меткой», по которой вы можете обращаться к объекту вбудущем. О б ъ ект D o g No 1 О б ъ ект D o g No 2 fid o rover spot
оглавление 15 Сценарии C# добавляют поведение к объектам GameObject 254 Добавление сценарияC# кобъекту GameObject 255 Написание кода C# для поворота сферы 256 Добавление точки прерывания и отладка игры 258 Использование отладчика для понимания Time.deltaTime 259 Добавление цилиндра для обозначенияосиY 260 Добавление полейдля углаповорота искорости 261 Debug.DrawRay и 3D-векторы 262 Запуск игры дляотображения лучав представленииScene 263 Поворот шара вокруг точки сцены 264 Эксперименты споворотамии векторамив Unity 265 Проявите фантазию! 266 Unity — не только мощный кросс-платформенный движок и редактор для построения 2D- и 3D-игр и моделирования. Также это отлич- ный способ потренироваться в написании кода C#. В этой лабо- раторной работе мы начнем писать код для управления объектами GameObject. Лабораторный курс Unity No 2 Написание кода C# для Unity
ог лав лен ие 16 5 Умейте хранить секреты Вам когда-нибудь хотелось, чтобы посторонние не лезли в ваши личные дела? Воти вашимобъектам этого иногда хочется. И если вы не желаете, чтобы чужие люди читали ваш дневник или просматривалибанковские выписки, хорошие объекты не позволяют дру- гим объектам копаться в их полях. В этой главе вы узнаете о мощи инкапсуляции — приеме программирования, который делает ваш код более гибким. Такой код проще использовать и его труднее использовать некорректно. Данные вашего объекта объявляются приватными, и к ним добавляются свойства, защищающие обращения к этим данным. Инкапсуляция Поможем Оуэну реализоватьброски на повреждения 268 Создание консольного приложения для вычисления повреждений 269 Разработка XAML для WPF-версии калькулятора повреждений 271 Код программнойчастидляWPF-калькулятора повреждений 272 Разговорза столом (или, может, дискуссия окубиках?) 273 Попробуем исправить ошибку 274 Использование Debug.WriteLine для вывода диагностической информации 275 Возможностьнекорректногоиспользования объектов 278 Инкапсуляция подразумевает ограничениедоступа кчастиданных класса 279 Применение инкапсуляциидля управления доступом к методам и полям класса 280 НоДЕЙСТВИТЕЛЬНОЛИ полеRealName надежно защищено? 281 Кприватным полям и методам могутобращаться толькоэкземпляры тогожекласса 282 Длячегонужнаинкапсуляция? Представьтеобъектв виде«черногоящика»... 287 Воспользуемся инкапсуляцией для улучшения классаSwordDamage 291 Инкапсуляция обеспечивает безопасность данных 292 Консольноеприложениедля тестирования класса PaintballGun 293 Свойстваупрощаютинкапсуляцию 294 Изменение метода Main для использования свойства Balls 295 Автоматически реализуемыесвойства упрощают ваш код 296 Использование приватногоset-методадля созданиясвойств, доступных толькодля чтения 297 Аесли потребуетсяизменитьразмер магазина? 298 Использование конструктора с параметрами для инициализации свойств 299 Передача аргументов при использовании ключевогослова"new" 300 RealName: "Herb Jones" Alias: "Dash Martin" Password: "the crow flies at midnight" S e c r et A g e n t  O B J E C T SwordDamage Roll MagicMultiplier FlamingDamage Damage CalculateDamage SetMagic SetFlaming
оглавление 17 6 Генеалогическое древо объектов Иногда люди ХОТЯТ быть похожими на своих родителей. Вы встречали объект, которыйдействует почти так, как нужно?Думали ли вы о том, что при изменении всего нескольких элементов класс сталбы идеальным? Наследование позволяетрасширять существующие к лассы, чтобы новый класс получал все поведение существующего — сохраняя при этом гибкость для внесения изменений, чтобы класс можно было адаптировать под любые конкретные требования. Наследование является одним из самых мощн ы х ин с трумен тов C#: в час тн ос ти, он о п омогает избегать ду блирован ия к ода, более адек в атн о моделировать реальный мир и в конечном итоге упрощает их сопровождение и снижаетриск ошибок. Наследование Вычисление повреждений для ДРУГИХ видов оружия 314 Команды switch для выбораиз нескольких кандидатов 315 И еще...Можноли вычислять повреждения откинжала?Отбулавы?И шеста? И... 317 Еслив ваших классах используется наследование, код достаточно написать только одинраз 318 Постройтемодельклассов: начните собщегоипереходитек конкретике 319 Как бы вы спроектировали симуляторзоопарка? 320 Уразных животных разное поведение 322 Каждый субклассрасширяет свой базовый класс 325 Расширениебазовогокласса 330 Субклассможетпереопределять методы для изменения или замены унаследованныхкомпонентов 332 Некоторыекомпоненты реализованы тольков субклассе 337 Анализ переопределенияв отладчике 338 Построение приложения для изучения virtual и override 340 Субклассможетскрыватьметоды базовогокласса 342 Использованиеключевых словoverride и virtual для наследования поведения 344 Еслибазовый класссодержит конструктор,ваш субклассдолженеговызвать 347 Субкласси базовый класс могут иметьразные конструкторы 348 Порадоделать приложениедля Оуэна 349 Построение системы управленияульем 356 Модель классов системы управления ульем 357 КлассQueen:как маткауправляетрабочими 358 Пользовательскийинтерфейс:добавление кода XAML главногоокна 359 Обратная связь направляет работу системыуправления ульем 368 Системауправления ульем работаетв пошаговом режиме... Преобразуем ее для работы в реальном времени 370 Экземпляры некоторых классов никогдане должны создаваться 372 Абстрактныйкласс —намереннонезавершенный класс 374 Уабстрактных методовнет тела 377 Абстрактныесвойстваработают как абстрактныеметоды 378 Смертельный ромб 381 An imal Picture Food H unger B oundar ies Loc ati on MakeNoise Eat S leep R oam Hippo MakeNoise Eat Sw im Dog MakeNoise Fe tch Breed Wo lf MakeNoise HuntWithPack Canine Eat Sleep AlphaInPack IsArboreal
ог лав лен ие 18 Построим игру в Unity! 384 Создайте новый материалв папке Materials 385 Создание бильярдного шара в случайной точке сцены 386 Применениеотладчика для понимания Random.value 387 Преобразование объектаGameObject взаготовку 388 Создание сценария дляуправления игрой 389 Присоединение сценарияк главнойкамере 390 Запуститесвой кодкнопкойPlay 391 Работа сэкземплярами GameObject вокне Inspector 392 Предотвращениеперекрытия шаров 393 Проявите фантазию! 394 Unity — не только мощный кросс-платформенный движок и редактор для построения 2D- и 3D-игр и моделирования. Также это отлич- ный способ потренироваться в написании кода C#. В этой лабо- раторной работе мы начнем писать код для управления объектами GameObject. Лабораторный курс Unity No 3 Экземпляры GameObject
ог л авл ение 19 7 Классы должны держать обещания Вам нужен объект для выполнения конкретной задачи? Используйте интерфейс. Иногда возникает необходимость сгруппировать объекты по выполняемым ими функциям, а не по классам, от которых они наследуют. На помощь приходят интерфейсы. Интерфейсы могут использоваться для определения конкретных задач. Любой экземпляр класса, реализующего интерфейс, гарантированно выполняет эту задачу независимо от того, с какими другими классами он св я зан . Чтобы э та с хема работала, к аждый к лас с, реал изую щий ин терфейс , должен гаран - тировать выполнение всех своих обязательств... иначе программа компилироваться не будет. Интерфейсы, приведение типов и is Улей под атакой! 396 Мы можем воспользоваться приведением типовдля вызова метода DefendHive... 397 Интерфейсопределяет методы исвойства, которыедолжны быть реализованы классом... 398 Потренируемся в использовании интерфейсов 400 Создатьэкземпляринтерфейсаневозможно, номожнополучить ссылку наинтерфейс 406 Ссылкинаинтерфейсыявляются обычными ссылкаминаобъекты 409 RoboBee 4000 можетвыполнять работу пчел без расходадрагоценного меда 410 СвойствоJob винтерфейсеIWorker —костыль 414 Использованиеisдля проверкитипаобъекта 415 Использованиеis для обращения к методам субкласса 416 Аеслимы захотим, чтобы другиеживотные плавалиилиохотилисьв стае? 418 Использованиеинтерфейсов для работы склассами, выполняющими одну задачу 419 ВC#такжесуществует другойинструмент для безопасногопреобразования типов:ключевое словоas 421 Примерповышающегоприведения типа 423 ПовышающееприведениепреобразуетCoffeeMakerв Appliance 424 Повышающиеи понижающие приведения типов также работают и синтерфейсами 426 Интерфейсы могут наследовать от других интерфейсов 428 Интерфейсы могут содержать статические компоненты 435 Реализации поумолчанию определяюттелометодов интерфейса 436 Добавлениеметода ScareAdults среализацией поумолчанию 437 Связывание данных обеспечиваетавтоматическое обновление элементов WPF 439 Связывание данных в системеуправления ульем 440 «Полиморфизм»означает, чтоодинобъектможетсуществовать в разныхформах 443 О б ъ е к т Q u e e n О б ъ е к т H i v e D e f e n d e r За щища ть улей любой ценой. Да, повелительница!
оглавление 20 8 Организация данных Данные невсегдабывают такими аккуратными и ухоженными, как нам хотелось бы. В реальноммире данные, как правило, не хранятся маленькими аккуратными кусочками. Нет, данныепоступают вагонами, штабелями и кучами. Для их систематизации нужны мощные инструменты, и тут вам на помощь приходят перечисления и коллекции. Перечисления — типы, позволяющие определять значения для классификации ваших данных. Коллекции — специальные объекты, способные хранить и сортировать данные, которые обрабатывает программа, и управлять ими. В результате вы можете сосредоточиться на основной идее программирования, оставив задачу управления данных коллекциям. Перечисления и коллекции Строки не всегда подходят для хранения категорий данных 446 Перечисления предназначены для работы снаборамидопустимых значений 447 Длясоздания колоды карт можновоспользоваться массивом... 451 Смассивамибывает неудобноработать 452 В списках можнохранить коллекции... чего угодно 453 Списки обладают большейгибкостью, чем массивы 454 Построим приложение для хранения обуви 457 В обобщенных коллекциях могутхраниться любые типы 460 Инициализаторы коллекций похожинаинициализаторы объектов 466 Создание списка уток 467 Списки удобны, но ссортировкоймогут возникнутьпроблемы 468 IComparable<Duck> помогает списку List сортировать объекты Duck 469 Использование IComparer для определения порядка сортировки 470 Создание экземпляракомпаратора 471 Компараторы могут выполнять сложныесравнения 472 Переопределение метода ToString позволяет объекту описатьсебя 475 Обновите циклы foreach, чтобы объекты Duck иCardвыводили свои описания на консоль 476 Использование Dictionary для хранения ключей и значений 482 Краткая сводка функциональности Dictionary 483 Построениепрограммыс использованием словаря 484 Другие разновидности коллекций... 485 Очередьработает попринципу FIFO — «первым вошел, первым вышел» 486 Стек работает попринципу LIFO— «последним вошел, первым вышел» 487 Упражнение:две колоды 492 Карта «Герцог быков». В природе не встречается.
ог л авл ение 21 Вывод текущего счета 494 Включениедвухрежимовв игру 495 Добавление игровогорежима 496 Добавление пользовательскогоинтерфейсак игре 498 Настройкаобъекта Text для вывода счетав UI 499 Кнопкадля вызоваметода, запускающегоигру 500 КнопкаPlay Again и текущий счет 501 Завершение кодаигры 502 Проявите фантазию! 506 На этом снимке экрана показана игра в рабочем режиме. Шары добавля- ются в сцену, а игрок может щелкать на них, чтобы получать очки. Когда на экране появится последний шар, игра переходит в режим завершения. На экране появляется кнопка Play Again, и новые шары перестают появляться. В предыдущей лабораторной работе Unity вы начали строить игру. Мы использовали заготовку для создания экземпляров GameObject, которые появлялись в случайных точках трехмерного пространства игры и летали по кругу. В этой лабораторной работе мы продол- жим с того места, на котором остановились в предыдущей главе; в ней вы сможете применить то, что узнали об интерфейсах C#, и многое другое. Лабораторный курс Unity No 4 Пользовательские интерфейсы
оглавление 22 9Контроль над данными LINQи лямбда-выражения Джимми —фанатКапитана Великолепного... 508 Использование LINQ для управления коллекциями 510 LINQ работаетс любыми реализациями IEnumerable<T> 512 СинтаксисзапросовLINQ 515 LINQ работаетс объектами 517 Использование запроса LINQв приложении для Джимми 518 Ключевое словоvarпозволяетC# определить тип переменной за вас 520 Запросы LINQ выполняютсятолькопри обращении к результатам 527 Использование запросов groupдляразделенияпоследовательностинагруппы 528 Использование запросов join для слияния данных из двух последовательностей 531 Использование ключевогословаnew для создания анонимных типов 532 Модульные тесты помогают понять, как работает код 540 Добавление проекта модульноготеста вприложение Джимми 542 Первый модульный тест 543 Написание модульного теста для метода GetReviews 545 Написание модульныхтестов для обработки граничныхслучаев и аномальных данных 546 Оператор=> и созданиелямбда-выражений 548 Тест-драйв лямбда-выражений 549 Рефакторингклоуновс использованием лямбда-выражений 550 Использование оператора ?: для принятиярешений влямбда-выражениях 553 Лямбда-выражения иLINQ 554 Запросы LINQ могутзаписываться ввиде сцепленных вызовов методовLINQ 555 Использование оператора => для создания выражений switch 557 ИсследованиеклассаEnumerable 561 Ручноесозданиепоследовательности споддержкойперебора 562 Упражнение:GoFish 567 Этим миром правят данные... И нам нужно знать, к ак в нем жить . Прошли те времена, когда можно было программировать днями и даже неделями, не имея дела с огромными объемами данных. В наши дниданные стали сутью любой программы. LINQ– технология C# и .NET, которая позволяет не только обращаться с запросами к данным в коллекциях .NET на интуитивно понятном уровне, но и группировать данные и выполнять слияние данных изразных источников. Модульные тесты помогутубедиться в том, что ваш кодработает так, как предполагалось. А когда вы освоитесь с задачей разбиения данных наблоки, с которымиудобно работать, вы можете воспользоваться лямбда-выражениями, провести рефакторинг кода C# и сделать его еще более выразительным.
ог л авл ение 23 Иногда настойчивость окупается. Пока что все ваши программы жили недолго. Они запу- скались, некоторое время работали и закрывались. Но этого недостаточно, когда имеешь дело с важной информацией. Вы должны уметь сохранять свою работу. В этой главе мы поговорим о том, как записать данные в файл, а затем о том, как прочитать эту информа- цию. Вы познакомитесь с потокамиданных, узнаете о сохранении объектов в файлах с ис- поль зов ан ием с ериализации, а так же ос воите работ у с шес тн адцатерич н ыми и дв оичн ы ми данными икодировку Юникод. 10 Прибереги последний байт для меня Чтение и запись файлов Длячтенияизаписи данныхв .NET используются потоки данных 570 Различные потоки для разных данных 571 ОбъектFileStream читает и записывает байты в файл 572 Запись текста вфайл затри простых шага 573 Дьявольский план Пройдохи 574 Использование StreamReader для чтения файла 577 Данныемогут проходить через несколько потоков 578 Работас файлами и каталогамис использованием статических классов File иDirectory 582 ИнтерфейсIDisposable обеспечиваеткорректное закрытиеобъектов 585 Предотвращение ошибокфайловойсистемы командамиusing 586 Потоки MemoryStream и хранение данных в памяти 587 При сериализации объектатакжесериализуются все объекты, накоторыеонссылается... 595 Использование JsonSerialization для сериализации объектов 596 JSONвключает толькоданные, нонеконкретныетипы C# 599 Следующий шаг: углубленный анализ данных 601 Строки C# кодируются в Юникоде 603 ПоддержкаЮникодав VisualStudio 605 . NE TиспользуетЮникод для хранениясимволови текста 606 C# может использоватьмассивы байтов для перемещения данных 608 Использование BinaryWriter для записи двоичных данных 609 Использование BinaryReader для чтения данных 610 Дамппозволяетпросматриватьбайты в файлах 612 Использование StreamReaderдля вывода шестнадцатеричногодампа 613 Использование Stream.Read для чтения байтов из потока 614 Аргументы командной строки 615 Упражнение:Hideand Seek 616 Eureka! 691171141011079733 6 9 117114101
оглавление 24 Создание новогопроекта Unity и началосоздания сцены 618 Настройка камеры 619 Создание объектаGameObject для игрока 620 Знакомствос системой навигации Unity 621 Создание сеткиNavMesh 622 Автоматическая навигация в игровой области 623 Камера направлена вниз, прямоугольник является областью просмотра. Крестиком обозначена точка, в которой был сделан щелчок. Метод проводит луч длиной до 100 единиц, который начинает- ся в текущей позиции камеры и проходит через точку, в которой щелк­ нул пользователь. Луч пересекается с полом в этой точке. Создавая сцену в Unity, вы создаете виртуальный 3D-мир, в ко- тором перемещаются персонажи вашей игры. Но в большинстве игр объекты окружающей обстановки не контролируются игроком напря- мую. Как же эти объекты определяют свое место в сцене? В этой лабораторной работе мы построим сцену из объектов GameObject и используем навигацию для перемещения персонажей по сцене. Лабораторный курс Unity No 5 Отслеживание лучей
ог л авл ение 25 Жизньи смерть объекта 630 Для принудительной сборки мусора используйте класс GC(осторожно!) 631 Когда именно выполняется финализатор? 633 Финализаторы не могутзависеть отдругихобъектов 635 Структура похожанаобъект... 639 ...ноне является объектом 639 Значения копируются, ссылки присваиваются 640 Структуры относятся к типам значений; объекты относятся к ссылочным типам 641 Стек и куча:подробнееопамяти 643 Параметры out и возвращениенескольких значений методом 646 Передача поссылке смодификатором ref 647 Необязательные параметры и значения поумолчанию 648 Ссылкаnullнеуказывает ни на какойобъект 649 Ссылочные типы, не допускающие null,помогутизбежатьNRE 650 Оператор объединения сnull ?? 651 Безопаснаяработастипами значений,допускающими null 652 Капитан... уже не такой Великолепный 653 Методы расширениядобавляютновоеповедение в существующие классы 657 Расширение фундаментальноготипа: string 659 Head First C# Глава 11 Четыре доллара Капитан Великолепный Смерть объекта ЧЕР ЕЗ НЕСКОЛЬКО МИНУТ И ТЫ, И МОЯ АРМИЯ СТАНЕТЕ МУСОРОМ... БОЙТЕСЬ СБОРЩИКА МУСОРА! – У МЕНЯ...ОСТАЛОСЬ... -ОХ! - ОДНО... ПОСЛЕДНЕЕ... ДЕЛО...
оглавление 26 12Борьба с огнем надоедает Обработка исключений Программисты не должны уподобляться пожарным. Вы усердноработали, штудировали справочники ируководства и, наконец, достигли вершины. Но вамдо сих пор продолжают зво- нить с работы по ночам, потому что программа упала или работает не так, как должна работать. Ничто так не выбивает из колеи, как необходимость устранять странные ошибки... но благодаря обработке исключений вы сможете написать код, который сам будетразбираться с возможными проблемами. А еще лучше, что вы можете планировать такие проблемы и восстанавливать работоспособность программы при их возникновении. О б ъ е кт Exc e p t i o n int[] anArray = {3, 4, 1, 11}; int aValue = anArray[15]; use r Т е п е р ь ваша программа боле е у с т о й ч и в a ! pub lic class Data { public void Process(Inpu t i) { try { if (i.IsB ad()) { explode (); } catch { Handle I t(); } } } О б ъ е к т An Программа вывода шестнадцатеричногодампа читает имя файлаиз командной строки 664 Когдаваша программа выдает исключение, CLRгенерирует объектException 668 Все объекты Exception наследуют от System.Exception 669 Длянекоторых файлов выводдампаневозможен 672 Чтопроисходитпри вызовенебезопасногометода? 673 Обработкаисключений сtry иcatch 674 Отслеживание передачи управления в try/catch 675 Код блокаfinally выполняется всегда 676 Перехват всех исключений 677 Использование исключения, подходящего для конкретной ситуации 682 Фильтры исключенийповышаютточность обработки исключений 686 Наихудший блок catch:универсальный перехват с комментариями 688 Временные решения допустимы(нотольковременно) 689 Вашкласстеперь умеетобрабатывать исключения Программадействительностабильна! Что за ерунда?
ог л авл ение 27 Продолжим стогоместа,накотором прервалась последняя лабораторнаяработаUnity 692 Добавление платформы всцену 693 Изменение настроек предварительного построения 694 Включениелестницыи наклонной плоскости вNavMesh 695 Решение проблем свысотой в NavMesh 697 Добавление препятствия в сетку NavMesh 698 Добавление сценария дляперемещения препятствия вверх ивниз 699 Проявитефантазию! 700 Это препятствиеNavMesh создает движущийся проемв NavMesh, который мешает игроку перемещаться вверхпо наклонной плоскости. Мы добавим сценарий, который позволяет игроку перетаскивать его мышью,чтобы блокировать и освобождать наклонную плоскость. В последней лабораторной работе Unity была создана сцена с по- лом (плоскость) и игроком (сфера с цилиндром). При этом использо- вался объект NavMesh, NavMesh Agent и отслеживание лучей, чтобы игрок следовал за щелчками в сцене. В этой работе мы восполь- зуемся навигационной системой Unity, чтобы объекты GameObject сами перемещались по сцене. Лабораторный курс Unity No 6 Перемещение по сцене
ог лав лен ие 28 I II Visual Studio для пользователей Mac Ката программирования для опытных и/или нетерпеливых Приложение 1. Проекты ASP.NET Core Blazor Приложение II. Ката программирования Зачем вам изучать C# 704 Созданиевашегопервогопроектав VisualStudio forMac 706 Давайте построим игру! 710 Какпостроить игру 711 СозданиепроектаBlazor WebAssembly в Visual Studio 712 Запуск веб-приложения Blazorв браузере 714 Visual Studioпомогает в написаниикодаC# 718 Завершениесоздания списка эмодзии вывод их в приложении 720 Перестановкаживотных в случайном порядке 722 Добавление новогопроектав систему управления версиями 728 Добавление кода C# для обработки щелчков 729 Назначение обработчиков щелчков кнопкам 730 Тестирование обработчика события 732 Диагностика проблемы в отладчике 733 Отладка обработчика события 734 Поиск ошибки, породившей проблему... 736 Добавление кода для сбросаигры припобеде 738 Добавление таймера 741 Добавление таймера вкод игры 742 Очисткаменюнавигации 744 Создание нового проекта Blazor WebAssembly App 747 Созданиестраницы сползунком 748 Добавление текстовогополя 750 Добавление селекторов для выборацветаи даты 753 Построение Blazor-версии приложения длявыбора карт 754 Страница состоит из строк и столбцов 756 Ползунокиспользуетсвязывание данных для обновления переменной 757 Добро пожаловать в забегаловку эконом-класса «УнеторопливогоДжо»! 760
Как работать с этой книгой Введение Не могу поверить, что они включили такое в книгу о C#! В этом разделе мы ответим на насущный вопрос: «Так почему они включили ТАКОЕ в книгу о C#?» Вамнравится? Книга стоит денег,которыевы заплатили занее, истанет лучшим подарком.
30 вв еден ие как работать с этой книгой 1 2 3 Для кого написана эта книга? Кому эта книга не подойдет? Если на вопросы... Есливыответите «да» на любой из следующих вопросов... ...вы отвечаетеположительно,тоэтакнигадлявас. .. . эта книга недля вас. Вы хотите изучать C# (а попутно обзавестись на- чальными знаниями о разработке игр и Unity)? Вы предпочитаете учиться практикуясь, а не просто читая текст? Вы предпочитаете оживленную беседу сухим, скучным академическим лекциям? 1 2 3 Вас больше интересует теория, чем практика? Вы скучаете или раздражаетесь при мысли о том, что вам придется работать над проектами и пи- сать код? Выбоитесь попробовать что-нибудьновое? Считаете, что книга по такой серьезной теме, как про- граммирование, должна быть неизменно серьезной? Обязательно ли знать другой язык программирования, чтобы воспользоваться этой книгой? КАТА ПРОГРАММИРОВАНИЯ Вы квалифицированныйразработчик с опытом работы на другихязыках итеперь хотитебыстро освоитьC# и Unity? Выпредпочитаетепрактику и считаете, что лучше всего с ходу взяться за код? Есливыответили«ДА!» на оба вопроса, мывключили ката программирования специально для вас. Дополнительную информацию можно найтивразделе «Ката программирования» вконце книги. Многие люди изучают C# как второй (третий, четвертый и т. д.) язык программиро- вания, но вы не обязаны быть опытным программистом, чтобы получить пользу от ч т ения книги. Если вы писали программы (даже маленькие!) на любом языке программирования, посе- щали вводные курсыв школеили интернете, писали сценарии для командной оболочки или пользовались языком базданных, тогда вы определенно обладаетенеобходимой под- готовкой для изученияэтойкнигиибудетечувствовать себя какрыба в воде. А еслиу вас меньше практического опыта, но вы все равно хотите изучать C#? Тысячи новичков — особенно тех,кто ранеестроилвеб-страницы или работал сфункциями Excel, — воспользовались этой книгой для изученияC#. Но если вы вообще ничего не знаете о программировании, мы рекомендуем начать с книги Эрика Фримена (Eric Freeman) «HeadFirst Learn To Code».
дальше4 31 в вед ение «Развесерьезныекнигипо программированию наC# такие?» «И почему здесь столько рисунков?» «Можно литак чему-нибудь научиться?» Мы знаем, о чем вы думаете Мозгжаждет новых впечатлений.Он постоянно ищет, анализиру- ет, ожидает чего-то необычного. Он так устроен, и это помогает нам вы жить . Сегодняувас меньшешансовстать закускойдлятигра. Но вашмозг все ещеначеку,а выпросто незамечаетеэтого. Как же нашмозг поступает со всеми обычными, повседневными вещами? Он всеми силами пытаетсяоградиться отних, чтобы они немешалиего настоящейработе— запоминанию того,что действи- тельно важно. Мозг несчитаетнужным сохранять скучную инфор- мацию. Она непроходитчерезфильтр,отсекающий «очевидно не- су ществен ное» . Но как же мозг узнает, что важно? Представьте, что вы отправи- лись на прогулку и вдруг прямо перед вами появляется тигр. Что происходитв вашейголове ив теле? Активизируются нейроны. Вспыхивают эмоции. Происходят хими- ческие реакции. Итогда ваш мозг понимает... Конечно, это важно! Не забывать! Атеперь представьте, что вы находитесь дома или в библио- теке,в теплом уютном месте,где тигры не водятся. Выучи- тесь — готовитесь кэкзамену.Или пытаетесь освоить слож- ную техническую тему, на которую вам выделили неделю... максимум десять дней. И тут возникает проблема: ваш мозг пытается оказать вам услугу.Он старается сделать так, чтобы на эту очевидно несу- щественную информацию не тратились драгоценные ресур- сы. Их лучше потратить на что-нибудь важное. На тигров, например. Или на то, что к огню лучше не прикасаться. Илина то, что ни в коем случаенельзя выкладывать фото с этой вечеринкина своейстраничкевFacebook. Нет простого способа сказать своему мозгу: «Послушай, мозг, ятебе,конечно, благодарен,но какой бы скучнойнибыла эта кни- га и пусть мой датчик эмоций сейчас на нуле, я хочу запомнить то, что здесь написано». И мы знаем, о чем думает ваш мозг В а ш м озг сч ит ает, что Э Т О ва ж но. Замечательно. Еще 737 сухих скучных страниц. В а ш м озг п о л а - г а ет, что Э Т О м о ж н о н ез а п о м и - н а ть.
32 вв еден ие как работать с этой книгой Эта книга для тех, кто хочет учиться Как мы что-то узнаем? Сначала нужно это «что-то» понять, а потом не забыть. Затолкать в голову побольше фактов недостаточно. Согласно новейшим исследованиям в области когнитивистики, нейробиологии и психологии обучения, для усвоения материала требу- ется нечто большее, чем просто прочитать текст. Мызнаем, как заставить ваш мозг работать. Основные принципы серии «Head First»: Наглядность. Графика запоминается гораздо лучше, чем обычный текст, и зна - чительно повышает эффективность восприятия информации(до89%, по данным исследований). Кроме того, материал становитсяболее понятным. Текст размещается на рисунках, к которым он относится, а не под ними или на соседней странице. Разговорный стиль изложения. Недавние исследования показали, что приразго- ворном стиле изложения материала(вместоформальных лекций) улучшение результа- тов на итоговом тестировании составляло до40%. Рассказывайте историю вместо того, чтобы читать лекцию. Не относитесь к себе слишком серьезно. Чтоскорее привлечет ваше внимание: занимательная беседа за столом или лекция? Активное участие читателя. Пока вы не начнете напрягать извилины, в вашей голове ничего не произойдет. Читатель долженбыть заинтересован в результате; ондолженрешать задачи, формулировать выводы и овладе- вать новыми знаниями. Адля этогонеобходимыупражнения и каверзные вопросы, в решении которых задействованы оба полушария мозга иразные чувства. Привлечение (и сохранение) внимания читателя. Ситуация, знакомая каждому: «Я очень хочу изучить это, но засыпаю на первой же странице». Мозг обращает внимание на интересное, странное, притягательное, неожиданное. Изучение сложной технической темы не обязано быть скучным. Интересное запоминается намного быстрее. Обращение к эмоциям. Известно, что наша способность запоминать в значи- тельной мере зависит от эмоционального сопереживания. Мы запоминаем то, что нам небезразлично. Мы запоминаем, когда что-то чувствуем. Нет, сентименты здесь ни при чем: речь идет о таких эмоциях, как удивление, любопытство, интерес и чувство «Да я крут!» при решении задачи, кото- руюокружающие считают сложной, — а може т быть, когда вы понимаете, что узнали столько интересного и нового, и теперь можете с пользой применять новые знания. Все эле- мен ты ма сси ва содержат ссылки. Сам массив явл яет ся объектом. Даже испуг помогает информации закрепить- ся в вашем мозгу. О б ъ е к т D o g ЯОБЕДАЮТОЛЬКО «УНЕТОРОПЛИВОГОДЖО»!
дальше4 33 в вед ение Есливыдействительно хотитебыстрееиглубжеусваивать новыезнания, задумайтесь надтем,как вы задумываетесь. Учитесьучиться. Мало кто из нас изучает теорию метапознания во время учебы. Нам положено учиться,но нас редко этому учат. Но раз вы читаете эту книгу, то, вероятно, хотите узнать, как програм- мировать на C#, и по возможности быстрее. Вы хотите запомнить про- читанное и применять новую информацию на практике. Чтобы извлечь максимум пользы из учебного процесса, нужно заставить ваш мозг вос- принимать новыйматериалкакНечтоВажное.Критичноедлявашегосу- ществования. Такоежеважное, кактигр.Иначевам предстоит бесконечная борьба с вашим мозгом, который всеми силами уклоняется от запоминания новой информации. Метапознание: наука о мышлении Как бы теперь заставить мозг все это запомнить... Как же УБЕДИТЬ мозг, что программирование на C# так же важно, как и тигр? Естьспособ медленный и скучный, а есть быстрый и эф- фективный.Первый основан на тупом повторении. Всем известно, что даже самую скучнуюинформацию можно за- помнить, если повторять ее снова и снова. При достаточном количе- стве повторений ваш мозг прикидывает: «Вроде бы несущественно, но раз одно и то же повторяется столькораз... Ладно, уговорил». Быстрый способ основан на повышении активности мозга, и особенно на сочетании разных ее видов. Доказано, что все факторы, перечисленные на предыдущей странице, помогают вашему мозгу работать на вас. Например, исследования показали, что размещениеслов внутририсунков(а не в подпи- сях, в основном тексте и т. д .) заставляет мозг анализировать связи между текстом играфикой,а это приводитк активизациибольшего количества ней- ронов. Больше нейронов — выше вероятность того, что информация будет сочтена важной идостойной запоминания. Разговорный стиль тоже важен: обычно люди проявляютбольше внимания, когда они участвуют вразговоре,так как им приходится следить за ходом бе- седы ивысказывать своемнение. Причеммозгсовершенно не интересует, что вы «разговариваете» с книгой!С другой стороны, еслитекст сух иформален, то мозг чувствует то же, что чувствуете вы на скучной лекции в ролипассив- ного участника. Его клонит всон. Норисунки иразговорныйстиль — это только начало .
34 вв еден ие как работать с этой книгой Вот что сделали МЫ Мы использовали рисунки, потому что мозг лучше приспособлен для восприятия графики, чем текста. С точки зрения мозга рисунок стóит тысячи слов. А когда текст комбинируется с графикой, мы внедряем текст прямо врисунки, потому что мозгприэтомработает эффективнее. Мы используем избыточность: повторяем одно и то же несколько раз, при- меняя разные средства передачиинформации, обращаемся к разным чувствам— и все для повышения вероятности того, что материалбудет закодирован в не- сколькихобластяхвашего мозга. Мыиспользуем концепциии рисункинесколько неожиданным образом, пото- му что мозг лучше воспринимает новую информацию. Кроме того, рисунки и идеиобычно имеют эмоциональное содержание, потомучтомозгобращает внима- ние набиохимию эмоций.То, что заставляет насчувствовать, лучше запоминает- ся, будь то шутка, удивление или интерес. Мыиспользуемразговорный стиль, потому что мозг лучше воспринимает инфор- мацию, когда вы участвуете в разговоре, а не пассивно слушаете лекцию. Это происходит ипричтении. В книгу включены многочисленные упражнения, потому что мозг лучше запо- минает, когда вы работаете самостоятельно. Мы постарались сделать их непро- стыми, но интересными— то, что предпочитает большинство читателей. Мысовместили несколько стилей обучения, потому что одни читатели любят по- шаговые описания, другие стремятся сначала представить «общую картину», а третьим хватаетфрагментакода. Независимо от ваших личныхпредпочтений полезно видеть несколько вариантов представленияодногои тогоже материала. Мыпостарались задействовать оба полушария вашего мозга: это повышает веро- ятность усвоения материала. Пока одна сторона мозга работает, другаяимеет возможность отдохнуть; это усиливает эффективность обученияв течение про- должительного времени. Аеще в книгу включены истории иупражнения, отражающие другие точкизре- ния. Мозг качественнее усваивает информацию, когда ему приходится оцени- вать ивыносить суждения. В книге частовстречаются вопросы, на которые не всегда можно дать простой ответ, потому что мозгбыстрееучитсяизапоминает, когда емуприходится что- то делать. Невозможно накачать мышцы, наблюдая за тем, как занимаются дру- гие. Однако мы позаботились о том, чтобыусилия читателейбыли приложены в верном направлении. Вам не придется ломать голову над невразумительными примерами илиразбираться в сложном, перенасыщенном техническим жарго- ном или слишком лаконичномтексте. В историях, примерах, на картинках использованы антропоморфныеобразы. Ведь вы человек. И вашмозгуделяет большевнимания людям, а невещам. Определяякласс,выопре- деляетеего методы, точно так жекакчертежопреде- ляет внешний вид дома. На основеодногочер- тежаможнопостро- итьсколькоугодно домов, аизодного класса можно полу- читьсколько угодно объектов. Беседа у камина КЛЮЧЕВЫЕ МОМЕНТЫ
дальше4 35 в вед ение Мы свое дело сделали. Остальное за вами. Эти советы станут от- правной точкой; прислушайтесь к своему мозгу и определите, что вам подходит, а что неподходит. Пробуйтеновое. Что можете сделать ВЫ, чтобы заставить свой мозг повиноваться 1 Не торопитесь. Чем больше вы поймете, тем меньше придется запоминать. Просточитатьнедостаточно. Когда книга задает вам вопрос,непереходитек ответу.Представьте, что кто-то действительно задает вам вопрос. Чем глубже ваш мозг будет мыслить, тем скорее вы пойметеизапомнитематериал. 2 Выполняйтеупражнения, делайте заметки. Мы включили упражнения в книгу, но выпол- нять их за вас несобираемся. И неразглядывайте упражнения. Беритекарандаш и пишите. Физи- ческиедействиявовремя обучения повышаютего эффективность. 3 Читайте врезки. Этозначит:читайтевсё.Врезки —частьосновного материала!Непропускайтеих. 4 Не читайте другиекниги после этой перед сном. Частьобучения(особенноперенос информации в долгосрочную память) происходит после того, как вы откладываетекнигу.Вашмозг несразуус- ваивает информацию. Если во времяобработки поступитноваяинформация,часть того,что вы узналиранее,можетбыть потеряна. 5 Пейте воду. И побольше. Мозгу нужна влага, так он лучше работает. Де- гидратация (которая может наступить еще до того, как вы почувствуете жажду) снижает ког- нитивны е ф ункц ии. 6 Говорите вслух. Речь активизирует другие участки мозга. Есливыпытаетесь что-то понять или запом - нить, произнесите вслух. А еще лучше— по - пробуйте объяснить кому-нибудь другому. Вы быстрее усвоите материал и, возможно, откроетечто-то новое. 7 Прислушивайтесь к своему мозгу. Следите за тем, чтобы ваш мозг неуставал. Если вы стали поверхностно воспринимать материал или забываете только что прочи- танное— пора сделать перерыв. 9 Пишите побольше кода! По-настоящему изучить языкC#, чтобы он за- крепился в вашем сознании, можно только одним способом: пишите побольше кода. Именно этимвам предстоит заняться,читая книгу. Подобные навыки лучше всего закре- пляются практикой. В каждой главе вы най- детеупражнения. Непропускайтеих. Небой- тесь подсмотреть врешениезадачи, если не знаете, что делать дальше! (Иногда можно застрять на элементарном.)Новсеравно пы- тайтесьрешать задачисамостоятельно. Пока вашкод неначнетработать, нестоитперехо- дить к следующим страницам книги. Вырежьте и прикрепите на холодильник. 8 Чувст вуй те! Вашмозг должен знать, что материал книги дейс твитель но важе н. П ер ежива йте за гер о - ев наших историй. Придумывайте собствен- ные подписи к фотографиям. Поморщиться над неудачной шуткой всеравно лучше, чем непочувствовать ничего.
36 вв еден ие как работать с этой книгой Упражнение Информация Этоучебник, а не справочник. Мы намеренно убралиизкнигивсе, что могло бы помешать изучению материала, над которым вы работаете. И при первом чтении книги начинать следует с самого начала, пото- му что книга предполагает наличие у читателя определенных знаний иопыта. Упраж нения О БЯЗАТЕ ЛЬНЫ к в ыполне нию . Упражнения и задачи являются частью основного содержания книги, а не дополнительным материалом. Некоторыепомогают запомнить но- вую информацию, некоторые — лучше понять ее, а какие-то — научить- ся применять ее на практике. Не пропускайте задачи. Необязательны- ми являются только «Ребусы вбассейне», но следует помнить, что они безусловно помогаютускорить процесс обучения. Повт оре ние при ме няе т ся нам ере нно. У книг этой серии есть одна принципиальная особенность: мы хотим, чтобы вы действительно хорошоусвоили материал. И чтобы вы запом- ниливсе,что узнали.Большинство справочников неставитсвоейцелью успешноезапоминание,но это несправочник,а учебник,поэтомунекото- рыеконцепцииизлагаютсяв книге по несколько раз. Выполняйте все упражнения! Предполагается, что читатели этой книги хотят научиться программи- ровать на C#. Что им не терпится приступить к написанию кода. И мы дали им массу возможностей сделать это. Во фрагментах, помеченных значком Упражнение!, демонстрируетсяпошаговоерешение конкретных задач. А вот картинка с кроссовкамисигнализируетонеобходимостиса- мостоятельного поиска решения. Небойтесь подглядывать на страницу с ответом! Просто помните, что информация лучше всего усваивается, когда вы пытаетесь решать задачкибезпостороннейпомощи. Мы также включили весь исходный код, необходимый для выполнения упражнений.Ондоступенна нашейстраницеGitHub: https:/github.com/head-first-csharp/fourth-edition. Упраж нения « Моз говой шт у рм» не им еют от вет ов. Внекоторых из них правильного ответа вообще нет, в другихвы долж- ны сами оценить, насколько правильны ваши ответы(это является ча- стью процесса обучения).Внекоторыхупражнениях«Мозговойштурм» приводятсяподсказки,которые помогутвам найти нужноерешение. Для наглядного пред- ставления сложных понятий в книге исполь- зуются диаграммы. Выполняйте все упражнения этого раздела. Не пропускайте эти упражнения, если вы действительно хотите изучить C#. А этим значком по- мечены дополнительные упражнения для люби- телей логических задач. Если вам не нравится за- путанная логика, скорее всего, эти задания вам тоже не понравятся. Возьми в руку карандаш e n e m y A ge n t s e c r e t Ag e n t 
дальше4 37 в вед ение Весь код в книге приводится наусловиях лицензии Open Source, разрешающей его использование в ваших проектах. Его можно загрузить на страницеGitHub (https :/ git hub. c om /hea d-fir st -cs har p/f ourt h-edit ion). Также в ы м ожете заг руз ить доку мен ты в фо рмате PD F, в которых рассматриваются возможности C#, не включенные в книгу (в том численекоторые новейшие средства C#). Как в книге используются игры В книге мы будем писать код для множества проектов, в том числе для игр. Мы сделали это не только потому, что мы любим игры. Игры — эффективный инструмент для изучения и преподавания C# по сле- дующим причинам: • Игры нам знакомы. Вам предстоит усвоить много новых концепций и идей. Их представление на зна- комой основе сделает процесс обучения более спокойным. • Игры упрощают объяснение проектов. Когда вы занимаетесь любым проектом в книге, прежде всего необходимо понять, что же именно вам предстоит сделать, — иногда это оказывается на удивление сложно. Когда в качестве проекта используются игры, это помогает вам быстрее определить, что от вас требуется, и погрузиться в код. • Создавать игры интересно! Ваш мозг намного более восприимчив к новой информации, когда она вам интересна, поэтому включение проектов с построением игр — идея абсолютно естественная. Мы используем игры для того, чтобы упростить изучение более широких концепций C#и программирования. Они являются важной частью книги. Обязательно выполняйте все игровые проекты в книге, даже если разработка игр не представляет для вас интереса. (Проекты «Лабораторный курс Unity» необязательны, но мы настоятельно рекомендуем вам выполнять их.) При написании книги использовались C# 8.0, Visual Studio 2019 и Visual Studio 2019 для Mac. Эта книга была написана для того, чтобы помочь вам в изученииC#. Группа Microsoft, которая зани- мается разработкой и сопровождением C#, периодически выпускает обновления языка. На момент публикации этой книги(речь идет об англоязычном издании. — Примеч. ред.) текущей версией языка былаверсияC#8.0. Мы также интенсивно используемVisual Studio, интегрированную средуразработ- ки (IDE)компанииMicrosoft, для изучения, преподавания и исследования возможностей C#. Снимки экранов,приведенныевкниге,былиполучены впоследнихверсияхVisualStudio2019иVisualStudio 2019для Macна моментпубликации. Инструкциипо установкеVisualStudio приводятся вглаве1,а ин- струкции по установкеVisualStudioforMac — в приложении «VisualStudio для пользователейMac». Несмотря на выходверсииC#9.0, вкоторойпоявились новые замечательные возможности, средства C#, описанные в данной книге, останутсябез изменений, так что вы сможете использовать эту книгу с буду- щимиверсиямиC#. Команды Microsoft, занимающиеся поддержкойVisual Studio иVisualStudioдляMac, периодически публикуют обновления, и эти изменения очень редко приводят к изменению внешнего вида снимков экрана. В разделах «Лабораторный курс Unity» используетсяUnity 2020.1 — новейшая версияUnity, доступная на моментсдачикнигив печать. Инструкциипо установкеUnity приведены впервомразделе«Лабора- торный курс Unity». Разработка игр... и не только
38 вв еден ие Научные редакторы «Если я видел дальше других, то потому, что стоял на плечах гигантов». — Исаак Ньютон Вкниге, которуювы читаете, оченьмалоошибок, и ее высокое качествов ЗНАЧИТЕЛЬНОЙмере является заслу- гой наших замечательных научных редакторов — гигантов, которые любезно подставили нам свои плечи. Группе редакторов:мы невероятно благодарны за всюработу, которуювы проделали. Спасибо! Линдси Бьеда — программист из Питтсбурга (штат Пенсильвания). В ее доме больше клавиатур, чем у любого другого человека. Когда Линдси не занимается программированием, она играет со своимкотом по имени Дэш ипьет чай. Сее проектами и рассуждениями можноознакомиться поадресу rarlindseysmash.com . Татьяна Мэк — независимый американский инженер, работающий напрямую с организациями для построениялогичныхи стройных продуктовисистем.Онаверит вто,чтостремление кдоступности, производительности иинклюзивностиможетулучшитьнашу социальнуюсредукакнацифровом,так и на физическом уровне. В этическом отношении она считает, что технологические специалисты могут избавляться отограничительныхсистем в пользу инклюзивных иориентированныхнасообще- ства. Доктор Эшли Годболд —программист, разработчик игр, автор, математик, преподаватель и мать. Она работает на полную ставку преподавателем программирования в крупной розничной сети, а также руководит небольшой инди-студией видеоигр Mouse Potato Games.Эшли является сертифицированным преподавателем Unity и ведет в колледжах учебные курсы по компьютерной теории, математике и разработке игры. Она является автором книг «Mastering Unity 2D Game Development (2nd Edition)» и «Mastering UI Development and Unity», а такжеразработчи- ком видеокурсов «2D Game Programming in Unity» и «Getting Started with Unity 2D Game Development». Имы хотим отвсей душипоблагодаритьЛизу Кельнер —этоуже 12-я (!!!) книга,которуюона редактируетдля нас. Огромное спасибо! Также хотим особопоблагодаритьДжоАлбахари иДжона Скита заихневероятную техническуюквалификацию, предельновнимательное и продуманное редактированиепервогоиздания, которое сталоодной из составляющих успехаэтойкниги за прошедшиегоды.Их информация нам сильнопригодилась— насамом деле намногосильнее, чем нам казалосьв товремя. На фотографиях не изображены (за- мечательные) редакторы второго и третьего изданий: Ребекка Дан-Кран, Крис Барроус, Джонни Халиф и Дэвид Стерлинг. А также редакторы первого издания: Джей Хилярд, Дэниел Киннаэр, Айям Сингх, Теодор Кассер, Энди Паркер, Питер Ричи, Кристина Пала, Билл Метельски, Уэйн Брэдни, Дэйв Мэрдок, и особенно Бриджитт-Жули Ландерс. Мы хотим особо поблагодарить наших замечательных читателей, прежде всего Алана Уэлетта, Джеффа Каунтса, Терри Грэхема, Сергея Кулагина, Уильяма Пива и Грега Комбоу, — которые сообщали нам о проблемах, обнаруженных при чтении книги, а так- же профессора Джо Варрассо из коллед- жа Мохаук, который включил нашу книгу в свой учебный курс. Огромное спасибо всем!!! Лиза Кельнер Эшли Год б олд Татьяна М эк Линдси Бьеда И мы пол- ностью согласны с ней!
дальше4 39 в вед ение Благодарности Редактор: Прежде всего мы хотим поблагодарить нашего замечательного редактора Ни- коль Тачи за все, что онасделала для нашей книги. Ты сделала все, чтобы книга увиделасвет, и предоставиламного полезной информации.Спасиботебе! Команда издательства O’Reilly: Ник оль Тач и И наконец... Большое спасибо Кэти Вайс из Indie Gamer Chick Fame за ее интересную информацию об эпилепсии, использованнуюв главе 10, а также за ее благородную борьбу за интересы людей, страдающих эпилепсией. Также поблагодарим Патрисию Аас за ее замечательное видео об изучении C# как второго языка, использованное в приложении «Ката програм- мирования», и за полученную от нее информацию о том, как упростить изучение C# для опытных программистов. Огромное спасибо нашим друзьям из Microsoft, помогавшим нам в работе над книгой, — ваша поддержка в этом проекте была просто замечательной. Мы благодарим Доминика Нахуса (поздравляем с рождением ребенка!), Джордана Мэттисена и Джона Миллера из команды Visual Studio для Mac, а также Коди Бейера, сыгравшего важную роль в организации на- шегосотрудничества. Спасибо Дэвиду Стерлингу за великолепнуюредактуру третьегоиздания и Иммо Ландверту, который помог нам определиться с темами, включенными в это издание. Дополнительно благодарим Мэдса Тор- герсена, руководителя программыразработки языкаC#, за еговеликолепноеруководствои советы завсе прошед- шие годы. Вы простовеликолепны. Мы особенно благодарны Джону Галлоуэю, который предоставил столько замечательного кода для проектов Blazor.Джон— старший руководитель программы разработки .NETCommunity Team. Он участвовал в написании нескольких книг о .NET, помогал проводить встречи .NET Community Standup и выступал соорганизатором под- каста «Herding Code». Огромноеспасибо! В издательстве O’Reilly очень много сотрудников, которых мы хотели бы поблагода- рить, надеемся, что никого не забыли. Как всегда, хотелось бы выразить признатель- ность Мэри Треслер, которая была с нами с первых дней нашего сотрудничества с O’Reilly. Спасибо выпускающему редактору Кэтрин Тозер, составителю алфавитного указателя Джоан Спротт и Рэйчел Хед за внимательную корректуру — все они помог - ли издать эту книгу за рекордно короткий срок. Большое сердечное спасибо Аманде Квинн, Оливии Макдональд и Мелиссе Даффилд, которые помогали поддерживать весь проект «находу». И еще вспомним наших других друзей из O’Reilly:Энди Орама, Джеффа Блайла, Майка Хендриксона и, конечно, ТимаО’Рейли. За то, чтовы сейчас читаете эту книгу, следует поблагодарить самую лучшую команду рекламистов: Марси Хэнон,КэтринБаррет,Сару Пейтон иостальныхзамечательных людей из города Сева- стополь, штат Калифорния. И еще хочется упомянутьнашихлюбимых авторов O’Reilly: Кэтрин Тозер Джон Галлоуэй • ПэрисБаттфилд-Эддисон,ДжонаМэннингаиТимаНагента—ихкнига «UnityGameDevelopmentCookbook» просто великолепна. Мы снетерпениеможидаемкниги «Head FirstSwift», написанной Пэрис и Джоном. • Джозефа Албахари и Эрика Иохансена, написавших бесценнуюкнигу«C# 8.0 in a Nutshell».
К диким гонкам готов! Начало работы с c# 1 Быстро сделать что-то классное! Хотите программировать быстро? C# — это мощный язык программирования. Благодаря Visual Studio вам не потребуется пи- сать непонятный код, чтобы заставить кнопку работать. Вместо того чтобы запоминать параметры метода для имени и для ярлыка кнопки, вы сможете создать действительно классное приложение. Зву- чит заманчиво? Тогда переверните страницу, и приступим к делу.
42 глава 1 Зачем вам изучать C# почем у сто ит из учать C# C#— простойсовременныйязык, на которомможнорешать совершенно неверо- ятные задачи. ИзучаяC#, выне ограничиваетесь тольконовым языком.C#делает доступнымдлявасцелый мир .NET —невероятномощной платформыс открытым кодом для построения самыхразнообразныхприложений. Visual Studio станет вашим окном в C# ЕсливыещенеустановилиVisualStudio2019,самоевремяэтосделать. Открой- те страницу https://visualstudio.microsoft.com и загрузитеверсиюVisualStudio CommunityEdition. (Еслионаужеустановлена на вашем компьютере,запусти- те программу установки Visual Studio, чтобы обновить набор установленных компонентов.) Если вы работаете в Windows... Проследите за тем, чтобы у вас была установлена поддержка кросс- платформенной разработки .NET Core и настольных средств разработки . NET. Только не включайте вариант Game development with Unity — позднее в книге мы займемсяразработкой 3D-игр на базеUnity, но поддержка Unity будет ус та новл ена о тдел ь но. Если вы работаете на Mac... ЗагрузитеизапуститепрограммуустановкиVisualStudio дляMac. Проследите за тем,чтобыбылаустановлена цель .NET Core. В большинствепроектов этой книги создаются консольные приложения .NET Core, которыеработают как в системе Windows, так и на Mac. В некоторых главахрассматриваются проекты (например, игра на поиск пар животных позднее в этой главе), являющиеся настольными проектами для Windows. Для этих проек- тов используйте приложение «ПроектыASP.NETCoreBlazer». В нем приведена полная заменадля главы 1, а также версииASP.NET Core Blazor для других проектов WPF. Убедитесь в том, что вы устанавлива- ете Visual Studio, а не Visual Studio Code. Visual Studio Code — прекрасный кросс­ платформенный редактор с откры- тым кодом, но он не так хорошо подходит для разработки .NET, как Visual Studio. Вот почему мы будем использовать Visual Studio в книге как учебный и аналитиче- ский инструмент. Проекты ASP.NET мож- но создавать и в системе Windows! Для этого про- следите за тем, что- бы при установке Visual Studio был включен ком- понент «ASP.NET and web development».
дальше4 43 начинаем программировать на C# Код C# можно писать и в Блокноте или в другом текстовом редакторе, но существует более удобный вариант. IDE (сокращение от Integrated Development Environment, т. е. «интегрированная среда раз- работки») включает в себя текстовыйредактор, визуальный конструктор, менеджер файлов, отладчик... словно многофункциональный инструмент для всех операций, которые могут понадобиться принаписаниикода. Перечислим лишь несколько задач,врешениикоторых вам поможетVisualStudio: Visual Studio ¦ инструмент для написания кода и изучения C# БЫСТРОЕсозданиеприложения.ЯзыкC#гибоки проств изучении, а Visual Studio позволяет автоматизировать многие операции, которые обычно приходится выполнять вручную.Примерыопераций,которыеVisualStudio делает за вас: Ì управлениефайламипроекта; Ì удобноередактированиекода проекта; Ì управление графикой, аудио, значками и другимиресурсами проекта; Ì отладка кода с возможностью выполнения впошаговом режиме. Построение удобных интерфейсов. Визуальный конструктор (Visual Designer) вVisual Studio IDE — одно из самых простых иудобных средств конструирования. Он делает за вас столько всего, что построение пользо- вательских интерфейсов для ваших программ становится одним из самых приятных аспектовразработки приложенийC#. Чтобы строить полно- функциональные профессиональныепрограммы,вам непридется тратить многие часы на возню с пользовательским интерфейсом (если только вы самиэтого незахотите.) Построение восхитительных в визуальном отношении программ. Объ- единяя C#сXAML(языквизуальнойразметкидля проектирования пользо- вательских интерфейсов длянастольныхприложенийWPF), выиспользуете один из самых эффективных инструментов для построения визуальных программ...ииспользуетеего дляпостроенияпрограмм,которыевыглядят такжевеликолепно, каки работают. Изучение и исследование C# и .NET. Visual Studio представляет собой инструментразработки мирового уровня, но к счастью для нас,это также великолепный учебный инструмент. Мы будем использовать IDE для ис- следования C#, что позволит вам быстро закрепить важные концепции программированияв вашем мозгу. 1 2 3 4 Visual Studio – замечательная среда разработки, но мы также будем использовать ее как учебный инструмент для изучения C#. Пользовательский интерфейс (UI) любого приложенияWPF строится набазе XAML (eXtensibleApplication Markup Language). Visual Studio содержит очень удобные средствадляработы с XAML. В книге Visual Studio будет называться просто «IDE». Если вы работае- те с Visual Studio на Mac, вы будете строить такие же великолепные при- ложения, но вместо XAML при этом будет использо- ваться комбинация C# с HTML.
44 глава 1 за дело Создание вашего первого проекта в Visual Studio Лучший способ изученияC# — непосредственное написание кода, поэтому мы воспользуемсяVisual Studio для создания нового проекта... инемедленно начнем писать код! Создайте новый проект по шаблону ConsoleApp (.NETCore). ЗапуститеVisual Studio 2019.Припервом запускена экранепоявится окно Create a new project с несколькими вариантами. Выберите вариант Create a new project. Есливызакроетеокно,небеспокойтесь: чтобывы- звать его обратно, достаточно выбрать вменю команду File>New>Project. Выберите тип проектаConsoleApp(.NETCore)инажмитекнопкуNext. Введите имя проекта MyFirstConsoleApp и нажмите кнопку Create. Прос мот рит е код нового при ложения . Присозданиинового проектаVisualStudio дает вамотправную точку длядальнейшейработы. Как только создание новых файлов для приложения будет завершено, на экране должен открыться файл с именем Program.cs со следующим кодом: 1 2 Сделайте это! Когда вы видите в тексте врезку Сделайте это! (или Добавьте!, или Принципы отладки), откройте Visual Studio и выполните приведенные инструкции. Мы точно расскажем, что следует делать и на что нужно обратить внимание, чтобы извлечь максимум пользы из приведенного примера. При создании нового проекта консольного приложения Visual Studio автоматически добавляет класс с именем Program. Класс изначально содержит метод с име- нем Main с одной командой для вывода строки текста на консоль. Классы и ме- тоды будут намного подробнее рассмат­ риваться в главе 2. Если вы используете Visual Studio для Mac, код этогопроекта— и всех проектов.NETCoreConsole App в этой книге — останется неизменным, но некоторыефункции IDE будут отличаться. За версией этой главы для Mac обращайтесь к при- ложению «VisualStudioforMacLearner’s Guide»).
дальше4 45 начинаем программировать на C# Запустите новое приложение. Приложение, котороесреда VisualStudio создала за вас, готово кзапуску.Найдитевверхнейчасти VisualStudioIDEкнопку с зеленым треугольником иименем вашего приложения инажмите ее: Просмотрите результаты выполнения программы. Когда вы запускаете вашу программу, на экране появляется окно отладочной консолиMicrosoft VisualStudio срезультатом выполнения программы: Лучшийспособ изучить язык— писать на нем побольшекода,поэтомув этойкнигемыбудем стро- ить множество программ.Многиеизнихбудутпроектами.NET CoreConsoleApp, поэтомудавайте повнимательнеепосмотрим, что жебыло сделано. Вверхней части окна выводятся выходныеданныепрограммы: Hello World! Далеепроисходит переходна следующую строку,за которой идетдополнительный текст: C:\path-to-your-project-folder\MyFirstConsoleApp\MyFirstConsoleApp\bin\Debug\ netcoreapp3.1\MyFirstConsoleApp.exe (process ####) exited with code 0. To automatically close the console when debugging stops, enable Tools-> Options->Debugging->Automatically close the console when debugging stops. Press any key to close this window . . . Это сообщение выводится в нижней части каждого окна отладочной консоли. Ваша программа вывела одну строку текста(Hello world!), после чего завершилаработу.Visual Studio оставляет окно вывода открытым, ожидая,когда вы нажмете клавишу для его закрытия; это позволяетвам просмотретьрезультаты до того,как оно исчезнетс экрана. Нажмитеклавишу,чтобы закрыть окно. Затемснова запуститепрограмму.Таквыбудетезапускать всепроекты .NET Core ConsoleApp,которыебудут создаваться вкниге. 3 4 Когда вы запуска- ете свое приложе- ние , оно выполняет метод Main, ко- торый вы- водит эту строку текста на конс оль .
46 глава 1 основы проектирования игр Давайте построим игру! Вытолько что построили свое первое приложениеC#, и это замечательно! А теперь попробуем создать что-то посложнее. Мыпостроим игру, вкоторой игрок долженподбирать пары животных. На экраневыводится квадратнаясетка с16живот- ными,а игрокщелкает на парах,чтобы ониисчезалисэкрана. Игра с подбором пар животных, которую мы построим. Игра является приложением WPF Консольныеприложенияпрекрасно подходят дляввода ивывода текста. Есливы хотите построить визуальное приложение, котороеработает в окне, придется использовать другую технологию. Вот почему игра с подбором пар животныхбудет приложением WPF. WPF (Windows Presentation Foundation) позволяет создавать настольные прило- жения,которыемогутработать влюбой версииWindows. Во многихглавахкнигибудет представленоодно приложениеWPF.В этойглавекратко представленоWPF иописаны средства дляпостроениякак визуальных,так иконсольных приложений. На экране выводятся восемь пар разных животных, случай- ным образом рас- пределенных в окне. Игрок щелкает на двух животных: если эти животные одинаковы, то пара исчезает из окна. Таймер следит за тем, сколько вре- мени потребовалось игроку для завер- шения игры. Цель игрока — найти все пары за мини- мальное количество времени. Построение проектов разных типов является важной частью изучения C#. Мы выбрали WPF (Windows PresentationFoundation) длянекоторых проектов в этой книге, потому что эта платформа предоставляет детализированные пользовательские интерфейсы, работающие во многих версиях Windows (даже вочень старых версиях, таких как Windows XP.) Но язык С# не ограничивается приложениями Windows! Вы работаете на Mac? Что же, вам повезло! Мы добавили длявас особыйучебный план с описанием Visual Studio дляMac. Обращайтесь к приложению «Краткий курс Visual Studio дляMac» в конце книги. В нем приведена полная альтернативная версия этой главы, а также Mac-версии всех проектов WPF, представленных в книге. К тому времени, когда вы завершите этот проект, вы гораздо лучше освоите те инструменты, которые будут использованы в книге для изучения и исследования возможностей C#. В версиях проектов WPF для Mac используется ASP.NET Core. Про- екты ASP.NET Core также мо- гут строиться и для системы Windows.
дальше4 47 начинаем программировать на C# Как построить игру Воставшейся частиэтойглавыбудетрассмотренпроцесс построенияигры, которыйсостоитиз нескольких шагов: 1. Сначаламысоздадим новыйпроект настольногоприложениявVisual Studio. 2. Затем мы воспользуемсяXAML дляпостроения окна. 3. Мы напишем код C# для размещения случайного эмодзи с животным вокне. 4. Игрок щелкает на парных эмодзи, чтобыудалить их из окна. 5. Наконец, чтобыигра сталаболее интересной, мыдобавим внее таймер. Обращайте внимание на врезки «Разработка игр... и не только», встреча- ющиеся в тексте книги. Мы будем использовать принципы проектирования для изучения и исследования важных концепций и идей программирования, применимых в любых проектах, не только в видеоиграх. MainWindow.xaml.cs MainWindow.xaml конСтруиро- вание окна напиСание кода C# обработка щелчков добавление та йм ера Что такое игра? Вроде бы ответ на этот вопрос совершенно очевиден. Но задумайтесь на минутку — все не так просто, как кажется на первый взгляд. • У всех ли игр есть победитель? У каждой ли игры есть конец? Не обязательно. Как насчет имитатора полета? Игры, в которой вы строите парк развлечений? Или таких игр, как The Sims? • Игры всегда интересны? Не для всех. Некоторым игрокам нравится процесс «гринда», когда им приходится де- лать одно и то же раз за разом;другим это кажется ужасным. • Всегда ли игры сопряжены с принятием решений, конфликтами или решением задач? Нет, не всегда. «Симу- ляторы ходьбы» — класс игр, в которых игрок просто исследует игровую среду, и часто в них вообще нет никаких головоломок или конфликтов. • Обычно довольно трудно четко определить, что же именно следует считать игрой. В учебниках по разработке игр можно найти множество разных определений. Для наших целей определим смысл «игры» (по крайней мере для хороших игр) следующим образом: Игра — программа, взаимодействовать с которой не менее увлекательно, чем создавать ее. На этот проект вам может потребовать- ся от 15 минут до часа (в зависимости от того, насколько быстро вы набирае- те текст). Мы луч- ше учимся, когда не спешим, так что не жалейте времени. Разработка игр... и не только Создание проекта
48 глава 1 так много файлов Создание проекта WPF в Visual Studio Запустите новыйэкземплярVisualStudio2019исоздайтеновый проект: Visual Studio создает папку проекта с множеством файлов При создании нового проекта Visual Studio добавляет новую папкус именемMatchGameизаполняетеефайлами ипапками, необходимыми для вашего проекта. Мы изменим два файла, MainWindow.xaml иMainWindow.xaml.cs . Играбудет строиться как настольное приложение с использованием WPF, поэтому выберите вариант WPFApp(.NETCore) ищелкните на кнопкеNext: VisualStudioпредложит вамнастроитьпроект.ВведитеимяпроектаMatchGame (при желаниитакже можно изменить папку для созданияпроекта): Щелкнитенакнопке Create. Visual Studio создает новый проект с именем MatchGame. Этот файл содержит код XAML, определяющий поль- зовательский интерфейс гл авн ого окна . MainWindow.xaml.cs В этом файле размещается код C#, который обеспечивает работоспособ- ность вашей игры. MainWindow.xaml Мы закончили с проектом консольного приложения, созданным в первой части этой главы, поэтому тот экземпляр Visual Studio можно закрыть. Если вы столкнетесь с какими-либо трудностями во время реализации проекта, перейдите на страницу GitHub и откройте видеоролик по ссылке: https://gitgub.com/head-first-csharp/fourth-edition
дальше4 49 начинаем программировать на C# НастройтесвоюIDEвсоответствии сприведенным снимком экрана.Сначалаоткройтефайл MainWindow.xaml— сделайте двойной щелчок на нем в окне Solution Explorer. Затем откройте окнаToolbox и Error List, выбрав их в меню View. Чтобы понять предназначение многихокон и файлов, достаточно присмотреться ких именам. По- пробуйте предположить, что делает каждая частьVisualStudio IDE. Мы привели один ответ, чтобы вам было про- ще начать работу. Попробуйте сделатьобоснованные предположениядля другихчастей IDE. Мы выбрали тему Light, так как светлые снимки экрана лучше выглядят на бумаге. Чтобы выбрать другую тему, выберите команду «Options...» в меню Tools и выделите раздел Environment. Конструктор позволяет ре- дактировать внешний вид ин- терфейса, пере- таскивая элемен- ты управления. Упражнения «Возьми в руку карандаш» являются обязательными. Это важная часть обучения, тре- нировки и совершенствования ваших навыков в C#. Вы заметили, что панель инструментов исчезает с экрана? Щелкните на этом значке, чтобы она оставалась на экране. Возьми в руку карандаш
50 глава 1 знайтеide Мы привели описания разных частей Visual Studio C# IDE. Надеемся, вы пра- вильнопредположили,длячего предназначено каждое окноикаждыйраздел IDE.Есливашиответывчем-тоотличаютсяот наших,неогорчайтесь!ВыОЧЕНЬ МНОГО узнаете об IDEвовремя практической работы. Инапомним еще раз:термины«VisualStudio» и «IDE» используются вкнигекак синонимы —в том числеи наэтой странице. В окне свойств выводятся свой- ства текущего выделенного элемента в кон­ структоре. Панель ин- струментов. На ней раз- мещаются визуальные элементы управления, которые можно пере- таскивать в окно кон- структора. В окне Error List выво- дятся ошибки в коде. На этой панели отобра- жается диагностическая информация. Файлы C# и XAML, которые создает IDE при добавлении нового проекта, появляются в окне Solution Explorer вместе с другими фай- лами приложения. Окно Solution Explorer в IDE позволяет переходить от одного файла к другому. Кон стр у ктор позволяет ре- дактировать внешний вид ин- терфейса, пере- таскивая элемен- ты управления. Значок в виде кнопки пере- ключает режим автоматического закрытия панели. Для окна Tool box эт от режим вклю - че н. Возьми в руку карандаш Решение
дальше4 51 начинаем программировать на C# В:Если код создается автоматически, не сводится ли изучение C# к изучению функ- циональности IDE? О: Нет . IDE прекрас но ге нериру ет ш абл онный код,но неболеетого.Она неплохо справляется с такими задачами, как выбор исходного состо- яния или автоматическое изменение свойств элементов в UI,но понять ,какуюработудолжна выполнять программа и какдостичь поставлен- нойцели, можете тольковы .ИхотяVisualStudio IDE считается одной из самых совершенных сред разработки, она не всесильна . Именно вы, а не IDE пишете код, непосредственно выпо лняю щий рабо ту прил ожени я. В:Что делать с ненужным кодом, автома- тически созданным IDE? О: Вы можете изменить или удалить его. IDE настроена так, чтобы генерировать код на основании наиболее распространенного использования элемента, но иногда это не то, чтовамтребуется.Все, что IDE делает за вас — каждую строку сгенерированного кода, каждый доба вл енный файл, — мож но изменит ь в ру чну ю или удобными средствами пользовательского инт ерфей са IDE. В:Почему вы предложили мнеустановить версиюVisualStudioCommunityEdition? Вы уверены, что для полноценного использо- вания материала книги мне не понадобится одна из платных версий VisualStudio? О: Вкниге нет ничего, что нельзя было бы сделать в бесплатной версии Visual Studio (которую можно загрузить на сайте Microsoft). Различия между Community Edition и другими версиями не помешают вам писать код на C# истроить полнофункциональные,завершенные прил ожения . В: В книгеупоминается о комбинации C# и XAML. Что такое XAML и как его комбини- ровать с C#? О: XAML(произносится«зэмл») — это язык разметки, позволяющий строить пользова- тельские интерфейсы для приложений WPF. XAMLбазируется на XML(такчтоесливамдо- водилось иметьделосHTML, вамбудетпроще). ПримертегаXAML,рисующего серыйэллипс: <Ellipse Fill="Gray" Height="100 " Width="75"/> Есливывернетесь к своему проекту и введете этот тег после <Grid> вкодеXAML, в середине окнапоявится серый эллипс.На то, что это тег, указываетсимвол <, закоторым следуетслово («Ellipse»). Все вместе составляет открываю- щий,или начальный, тег. ТегEllipseобладает тремясвойствами: однозадает серыйцветза- ливки,а двадругихопределяютегоширину ивы- соту.Тег завершается символом/>,нонекоторые теги XAML могут включать в себя другие теги. Данный тег можнопревратить в контейнерный, заменив /> на >, добавивдругиетеги(которые, в свою очередь, могут содержать внутренние теги) и завершив конструкцию закрывающим, или конечным, тегом: </Ellipse>. Вкнигевыузнаете,какработаетXAML,иосвоите много других теговXAML. В: Мой экран отличается от вашего!Одних окон вообщенет, другиенаходятся в других местах. Я сделал что-то не так?Каквернуть- ся к исходной настройке? О: Чтобы вернуться к настройкам по умол- чанию, выберите команду Reset Window Layout в меню Window — IDE восстановит стандартную конфигурацию окна. Затем при пом ощи ко манды Vi ew4O the r Wi ndow s можно открыть окнаToolbox и Error List, чтобы ваше окно IDE не отличалось от приведенного. Visual Studio генерирует код, который может стать заготовкой для построения вашего приложения. Но только вы отвечаете за правильность работы этой программы. Не пропускайте эти разделы. В них часто приводятся ответы на самые насущные вопросы, а также встреча- ются вопросы, которые возникают у других читателей. Кстати, многие из этих вопросов были заданы чита- телями предыдущих изданий книги! Панель инструментов закрывается по умол- чанию. Воспользуйтесь значком с изображе- нием канцелярской кнопки в правом верх- нем углу, чтобы она оставалась на экране. часто Зад авае м ые вопросы
52 глава 1 использование xaml Вы здесь! MainWindow.xaml.cs MainWindow.xaml Создание проекта конСтруиро- вание окна НаписаНие кода C# ОбрабОтка щелчкОв Добавление тайм е ра В начале каждого раз- дела проекта будет приведена «карта», которая поможет вам представить ме- сто текущего этапа в общей картине. Построение окна с использованием XAML Итак,среда VisualStudio создала проектWPF.Пришловремя поработать сXAML. XAML(сокращениеот eXtensibleApplicationMarkupLanguage) — чрезвычайногибкий языкразметки, используемыйразработчикамиC#дляпостроенияпользовательскихинтерфейсов.Присозданииприло- женийиспользуютсядве разновидностикода.Сначаларазработчикстроит пользовательскийинтерфейс (UI)набазе кода XAML, а затем добавляет код C#, обеспечивающий непосредственную работу игры. Есливыкогда-либо использовалиHTML для создания веб-страниц, наверняка XAML покажется вам знакомым. Маленькийпример созданияокна вXAML: <Window x:Class="MyWPFApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="This is a WPF window" Height="100" Width="400"> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock FontSize="18px" Text="XAML helps you design great user interfaces."/> <Button Width="50" Margin="5,10" Content="I agree!"/> </StackPanel> </Window> Атеперь посмотрим,каквыглядит окно,когда WPF отображает(рисует)его наэкране. Окно содержит два видимыхэлемента управления: элементTextBlock длявывода текста иэлементButton,на котором пользователь может щелкать. Ониразмещаются на невидимом элементе StackPanel, который обеспе- чивает их отображениеповерх друг друга. Посмотрите, как выглядят элементы на следующем снимке экрана,затем вернитесь кXAML инайдитетеги TextBlock иButton. Элемент TextBlock пред- назначен для вывода блока т екс та. Номерами обозначены части UI, соответствующие нумерации в код е XAML. 3 3 1 1 2 2 Найдите соответствующие номера на следующем снимке. Мы добавили ну- мерацию в части XAML, в кото- рых определяется текст.
дальше4 53 начинаем программировать на C# Построение окна для игры Дляработыприложенияпонадобитсяграфическийинтерфейс; объекты, обеспечивающие работуигры; иисполняемыйфайл,который можно будет запустить. На первый взгляд кажется, что нам предстоит грандиозная работа, но к концу этой главы всебудет готово, а вы получите представление о том, как использовать VisualStudio для построенияэффектных приложенийWPF. Окноприложения,которое мы собираемся построить,имеетследующую структуру: Макет окна представляет собой сетку из четырех столбцов ипяти строк. Каждое изображение животного выводится в элементе TextBlock. Таймер выводится в нижней строке в элементе TextBlock, распространяющемся на все четыре столбца. Знание XAML — важный навык для разработчика C#. Возможно, вы думаете: «Минутку! Книга называется “Head First. Изучаем C#”. Зачем тратить столько временинаXAML?» ПриложенияWPF используютXAML для построения пользовательского интерфейса — каки другие видыпроектовC#,XAML может использоватьсянетолько внастольныхприложениях,теженавыки могут применятьсядля построениямобильных приложенийC#дляAndroidиiOSнабазетехнологии Xamarin Forms, использующейразновидность XAML (с немного отличающимся набором элементов). Именнопоэтому построение пользовательских интерфейсов набазеXAML является важным навы- ком для каждого разработчика C#, и вэтой книге вы еще многое узнаетео XAML. Мы покажем,как строить код XAML шаг за шагом — средства конструктора XAML вVisual Studio2019позволяютпо- строить пользовательский интерфейс в визуальномрежиме,без ввода значительного объема кода. Повторимдля полной ясности: XAML— код, определяющий пользовательскийинтерфейс. C#—код,определяющийповедение. РЕ Л АКС
54 глава 1 начинаем проектировать UI Определение размера окна и текста заголовка в свойствах XAML Начнемспостроенияпользовательскогоинтерфейса для игрыс подборомпар.Первое, чтонеобходимо сделать, — уменьшить ширинуокна иизменить егозаголовок.Заодновы познакомитесь с конструктором XAMLVisualStudio— мощныминструментом дляпостроенияэффектныхпользовательскихинтерфейсов ва ших п р ило жений. В ыде лит е гла вное окно. Сделайте двойной щелчок на файлеMainWindow.xaml на панели Solution Explorer. Visual Studio немедленно открываетфайл в конструктореXAML. 1 В конс тр ук- торе выво- дится пред- варительное изображение редактиру- емого окна. Любые измене- ния, которые вы вносите, отражаются в выводимом ниже коде XAML. Также мож- но вносить изменения в код XAML и немедлен- но видеть результаты в верхней об- ласти пред- варительного просмотра. Двойной щелчок на файле панели Solution Explorer открывает этот файл в соответствующем редакторе. Файлы с кодом C# и расширением .cs открываются в редакторе кода. Файлы XAML с расширением .xaml открыва- ются в конструкторе XAML. Эти четыре кнопки предназначены для вклю- чения линий сетки, включения режима при- вязки (в котором элементы автоматически выравниваются по границам друг друга), переключения фона и включения привязки к линиям сетки. Используйте раскрывающийся список выбора масштаба для того, что- бы ограничиться небольшой частью окна или просмотреть все в целом.
дальше4 55 начинаем программировать на C# Когда вы изменяете свойства в тегах XAML, изменения автоматически отражаются в окне свойств. Когда вы используете окно свойств для модификации пользовательского интерфейса, IDE обновляет код XAML. Изменение текста заголовка окна. Найдите в конце тега Window следующую строку кода XAML: Title="MainWindow" Height="450" Width="400"> изамените текстзаголовка на Findall of thematchinganimals: Title="Find all of the matching animals" Height="450 " Width="400 "> Измененияотражаются вразделе Common окна свойств, — и что еще важнее, вполосе заголовка окна теперь выводится новый текст. 3 Изменение размеров окна. Переместитемышь вредакторXAMLи щелкнитевлюбойточкевпределахпервыхвосьми строк кодаXAML.Кактолько выэто сделаете, напанелиProperties должен появиться перечень свойств. РаскройтеразделLayoutи изменитеширину(Width)на400.Окно напанели конструктора должно автоматически уменьшиться. Присмотритесь к коду XAML— свойствоWidthтеперьравно400. Если изменить ширину с 800 на 400 в редакто- ре XAML, окно свойств будет автоматически об нов ле но. 2
56 глава 1 начи наем п ро екти ро вать UI В вашей IDE что-то мо - жет выглядеть иначе. Все снимки экрана были сделаны в Visual Studio Community 2019 для Windows. Пользовате- ли издания Professional или Enterprise могут заметить ряд второ- степенных отличий. Не беспокойтесь, рабо- тать все будет точно так же. Ширины столб- цов и высоты строк в кон- структоре соответству- ют свойствам в опред елениях строк и столб- цов в XAML. Добавление строк и столбцов в сетку XAML Можетпоказаться, что главное окно остается пустым, но присмотритесь повнимательнее к нижней части XAML.Видите строку <Grid>, за которой следует еще одна строка </Grid>?На самом деле окно содержитсетку, но вней пока нетнистрок,ни столбцов. Давайтедобавим строку. Переместитеуказатель мыши в левую часть окнав кон- структоре. Когда над указателем появится знак «+», щелкнитекнопкоймыши,чтобы добавить строку. Пользовательский интерфейс приложенияWPF строится из элементов управления: кнопок, надписей, флажков и т. д . Сетка является контейнером — особой разновидностью элемента, которая может содержать другие элементы. Она использует строки и столбцы для определения макета. Выувидитечисло,за которым следуетзвездочка,ивокнепоявляетсягори- зонтальная линия. Вы только что добавили новую строкув сетку!Добавим другиестроки и столбцы: Ì Повторитеоперациюеще четырераза, чтобысеткасодержала пять строк. Ì Наведитеуказатель мышина верхнюю часть окна ищелкните,чтобыдо- бавить четырестолбца. Окно должно выглядеть так,как показано ниже (увасчислабудутдругими— это нормально). Ì Вернитесь к коду XAML.Теперь он содержит набор тегов ColumnDe­ finition и RowDefinition, соответствующих добавленным строкам и столбцам. Такие врезки преду- преждают вас о важ- ных, но часто не - очевидных моментах, которые могут сбить вас с толку или сни- зить темп обучения. Будьте осторожны!
дальше4 57 начинаем программировать на C# Выравнивание размеров строк и столбцов Животные, которых игрок должен распределять по парам, должны на- ходиться на равныхрасстояниях. Каждое животное находится в ячейке сетки,а сетка автоматически подстраиваетсяподразмеры окна,поэтому все строки и столбцы должны иметь одинаковые размеры. К счастью, XAML позволяет легко изменять размеры строк и столбцов. Щелкните на первом теге RowDefinition в редакторе XAML, чтобы вывести его свойства в окнеProperties: Когда этот квадратик заполнен, это означает, что свойство не име- ет значения по умол- чанию. Щелкните на квадратике и выберите в открывшемся меню команду Reset, чтобы вернуть ему значение по умолчанию. Перейдите в окно свойств и щелкните на квадратике справа отсвойстваHeight, после чего выберитевоткрывшемся меню команду Reset. Минутку, что происходит? Как только вы это делаете, строка исчезает из конструктора. Впрочем, на самом деле она несовсем исчезает — она просто становится оченьуз- кой.Сбросьте свойство Height для всех строк. Затем сбросьте свойствоWidthдлявсехстолбцов. После этого сеткадолжна со- стоять из четырехстолбцоводинаковойширины ипятистрок одинаковойвысоты. Вот что вы должны видеть в конструкторе: Щелкните на этом тексте. <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition/> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> </Grid> А вот чтовы должны видеть в редакторе XAML между открывающим тегом <Window...> и закрывающим тегом </Window>: Код XAML для создания сетки из че- тырех столбцов одинаковой ширины и пяти строк одинаковой высоты. Попробуйте прочитать код XAML. Если ранее вы никогда не работали с HTML или XML, он может пока- заться мешаниной <угловых скобок> и /косых черт. Чем внимательнее вы будете к нему присматривать- ся, тем более осмысленным он вам будет казаться.
58 глава 1 управление макетом Размещение элементов TextBlock в сетке WPFиспользует элементыTextBlockдлявывода текста;мывоспользуемся имидлявывода изображений животных. Добавим такой элемент вокно. Раскройте раздел Common WPF Controls на панели инструментов и перетащите элемент TextBlock в ячейку, находящуюся во втором столбце и второй строке. IDE добавляет тег TextBlock между началь- ным и конечным тегом Grid: <TextBlock Text="TextBlock" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="560,0,0,0" TextWrapping="Wrap" /> Код XAML для этого элемента TextBlock содержит пять свойств: Ì Textсообщает TextBlock,какойтекст долженвыводитьсявокне. Ì HorizontalAlignment выбирает режим горизонтального вырав- нивания текста по левомукраю,по правому краю илипо центру. Ì VerticalAlignment выбирает режим вертикального выравни- ваниятекста по верхнемукраю,по нижнему краю илипо центру. Ì Marginзадает смещение от краев контейнера(верх,низ, стороны). Ì TextWrappingуказывает,нужно лидобавлятьразрывыстрокдля переноса текс та. Увассвойства могутследоватьвдругом порядке, а свойствоMargin может содер- жать другие числа, потому что они зависят от того, в какое место ячейки вы пере- тащили элемент.Все эти свойства можноизменитьилисброситьвокнесвойствIDE. Изображенияживотных должныбыть достаточно крупными, поэтому раскройте раздел Text в окне свойств и выберитеразмер шрифта36 px. Затем перейдите вразделCommon изадайте свойствуTextзначение ?, чтобы вэлементе выводилсявопросительный знак. Щелкните на этом квадра- тике и выберите команду Reset, чтобы сбросить поля. Свойство Text (из раздела Common) задает текст элемента TextBlock. Щелкнитенаполепоискавверхней части окна свойств.Вве- дитеслово wrap,чтобы найтисвойства с соответствующими именами. Значение свойства TextWrapping можно сбросить припомощи квадратика в правой частиокна. Когда вы перета- скиваете элемент с панели инстру- ментов в ячей- ку, IDE добавля- ет в XAML тег TextBlock и задает номер строки, столбца и величи- ну отступов.
дальше4 59 начинаем программировать на C# Сетка содержит один элемент TextBlock — неплохое начало! Но для вывода всех животных понадобятся 16 элементовTextBlock. А вы сможете предположить, как добавить код XAMLдля включения идентичных элементов TextBlock во все ячейки первых четырех строк сетки? Дляначалапросмотрите только что созданный тег XAML. Он должен выглядеть примерно так (свойства могут следовать в другом порядке, и мы добавили разрыв строки, чтобы код XAML лучше читался): <TextBlock Text="?" Grid.Column="1" Grid.Row="1" FontSize="36" HorizontalAlignment="Center" VerticalAlignment="Center"/> Ваша задача — продублировать элемент TextBlock, чтобы все 16 верхних ячеек сетки содержали одинаковые элементы. Чтобы выполнить это упражнение, необходимо добавить в приложение еще 15 элементовTextBlock. Несколько обстоятельств, о которых следует помнить: • Нумерация строк и столбцов начинается с 0 — значения по умолчанию. Таким образом, если опустить свой- ство Grid.Row или Grid.Column, элемент TextBlock будет отображаться в левой ячейке строки или верхней ячейке столбца. • Вы можете отредактировать пользовательский интерфейс в конструкторе или же скопировать и вставить код XAML. Опробуйте оба варианта — посмотрите, какой из них покажется вам более удобным! Такие упражнения еще не раз встре- тятся вам в книге. Они дают вам возможность потренироваться в написании кода. И помните: вы всегда можете заглянуть в решение! В:Когда я сбросил значения высоты первых четырех строк, они исчезли, а потом вернулись, когда я сбросил высоту последней строки. Почему это произошло? О:Вам кажется, что строки исчезают, потому что по умолчанию сеткиWPF используют пропорциональныеразмеры строк и столбцов. Если высота по- следней строки была равна 74*, то при назначении первым четырем строкам высоты по умолчанию 1* размеры строки изменились таким образом, что первые четыре строки занимают 1/78 (или 1.3%) высоты строки, а последняя строка занимает 74/78 (или 94.8%) высоты, так что первые строки кажутся совсем маленькими. Как только вы возвращаете последней строке высоту по умолчанию 1*, размеры сетки автоматически изменяются так, чтобы каждая строка занимала20% высоты. В: Когда я назначаю окну ширину400, в каких единицах задается это значение? 400 — это много или мало? О:ВWPFиспользуются аппаратно-независимые пикселы ,которые всегда за- нимают1/96дюйма.Это означает,что 96пикселов всегдасоответствуют1дюйму на экранебез масштабирования.Ноесливы возьметелинейку и измеритеокно, может оказаться, что его ширина немного отличается от 400 пикселов(около 4.16 дюйма.) Дело втом, что в Windows используется полезнаяфункциональ- ность изменения масштаба экрана, чтобы приложения не казались слишком маленькими, если вместо монитора используется телевизор, на который вы смотритеиздругогоуглакомнаты.Благодаряаппаратно-независимым пикселам приложения WPF хорошо смотрятся при любом масштабе. Выходит, если я хочу, чтобы один столбец был вдвое шире других, я просто назначаю ему ширину 2*, а сетка сделает все остальное. часто Задава ем ые вопросы Упражнение
60 глава 1 окно создано, пора заняться программированием Ниже приведен код XAML для 16 элементов TextBlock с изображением животных — все они полно - стью идентичны, если не считать свойств Grid.Row и Grid.Column, которые размещают по одному элементу TextBlock в каждой из 16 ячеек сетки. (Тег Window остается тем же, поэтому мы его не приводим.) <Grid> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition/> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <TextBlock Text="?" FontSize="36" HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="?" FontSize="36" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column="1"/> <TextBlock Text="?" FontSize="36" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column="2"/> <TextBlock Text="?" FontSize="36" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column="3"/> <TextBlock Text="? " FontSize="36" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="1 "/> <TextBlock Text="? " FontSize="36" Grid.Row="1 " Grid.Column="1 " HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="? " FontSize="36" Grid.Row="1 " Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="? " FontSize="36" Grid.Row="1 " Grid.Column="3 " HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="? " FontSize="36" Grid.Row="2 " HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="? " FontSize="36" Grid.Row="2 " Grid.Column="1 " HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="? " FontSize="36" Grid.Row="2 " Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="? " FontSize="36" Grid.Row="2 " Grid.Column="3" HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="? " FontSize="36" Grid.Row="3 " HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="? " FontSize="36" Grid.Row="3 " Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="? " FontSize="36" Grid.Row="3 " Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="? " FontSize="36" Grid.Row="3 " Grid.Column="3" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> У этих четырех элементов TextBlock свойство Grid.Row равно 1, поэто- му они находятся во второй строке сверху (так как номер первой строки равен 0). Нормально, если вы включили свойства Grid.Row или Grid.Column со значением 0. Здесь они не указаны, потому что 0 явля- ется значением по умолчанию. А так выглядят определения строк и столбцов после вы- равнивания размеров. Так окно выглядит в кон- структоре Visual Studio после добавления всех элементов TextBlock. Вроде бы много кода, но на самом делеэто одна и та же строка, повторенная 16раз снебольшими вариациями. Каждая строка, начинающаяся с <TextBlock, содержит одинаковый набор из четырех свойств (Text, FontSize, HorizontalAlignment и VerticalAlignment). Различаются только свойстваGrid.Row и Grid. Column. (Свойства могут следовать в любом порядке.) с т р о - к а 0 с т р о - к а 1 с т р о - к а 2 с т р о - к а 3 с т р о - к а 4 стол- бец 0 стол- бец 1 стол- бец 2 стол- бец 3 Упражнение Решение
дальше4 61 начинаем программировать на C# Теперь можно переходить к написанию кода игры Конструирование главного окна завершено— хотябы настолько, чтобы моглазаработать следующая часть игры. Теперь добавим код C#. Когда вы вводите код C#, он должен быть абсолютно правильным. Некоторые считают, что настоящим разработчиком становятся только после того, как впервые проведут несколько часов в поисках неправильно по- ставленной точки. Регистр символов важен: SetUpGame и setUpGame — раз- ные имена. Лишние запятые, круглые скобки, точки с запятой и т. д . могут нарушить рабо- тоспособность вашего кода, или , что еще хуже, изменить ваш код так, что он будет успешно строиться, но работать совершенно не так, как предполагалось. Функция IDE IntelliSense по- могает избежать подобных проблем... но и она не сможет сделать все за вас. Ранеемы редактировали код XAML в файлеMainWindow.xaml. В нем размещаются все элементы струк- туры окна — этот код определяет внешний вид и макет окна. Теперь мы начнем работать над кодом C# из файлаMainWindow.xaml.cs. Он называет- ся кодом программной части окна, потому что он объединяется сразметкой в файле XAML. Именно поэтому код хранится в фай- ле с таким же именем, но с дополнительным расширением «.cs». Мы добавим в этот файл код C#, который определяет поведе- ние игры, включая код добавления эмодзи в сетку, код обработки щелчков мышью и обеспечения работы таймера. Вы здесь! MainWindow.xaml.cs MainWindow.xaml Создание проекта Конструиро- вание оКна напиСание кода C# ОбрабОтка щелчкОв Добавление та йм ера Будьте осторожны!
62 глава 1 добавление методадля подготовки игры Итак, пользовательский интерфейс готов, теперь можно переходить к написанию кода самой игры. Для этого мы сгенерируем метод (сходный с методом Main, приведенным ранее)идобавим внего код. Генерирование метода для настройки игры Используйте вкладки в верхней части окна для переключения между редакто- ром C# и конструктором XAML. Сгенерируйте эт о! Откройте файл MainWindow.xaml.cs в редакторе. Щелкните на кнопке с треугольником в файле MainWindow.xaml на панели Solution Explorer, а затем сделайте двойной щелчок на файле MainWindow.xaml. cs,чтобы открыть его вредакторекода IDE. Нетрудно заметить,чтов файлеуже присутствует код.VisualStudio поможет добавить в него новый метод. 1 Сгенерируйте метод с именем SetUpGame. Найдите в открывшемся коде следующийфрагмент: public MainWindow(); { InitializeComponent(); } ЩелкнитепослестрокиInitializeComponent();, чтобыустановить курсор непосредственно за символом ;. Нажмите Enter дважды, а затем введите SetUpGame();. Как только вы введете символ ;, под SetUpGame появляется красная волнистая линия. Щелкните на слове SetUpGame — в левой части окна появляется изображение лампочки. Щелкните на нем, чтобы открыть менюQuickActions исгенерировать методс его помощью. 2 Когда вы щелкаете на значке Quick Actions, на экране появляется контекстное меню со списком действий. Если какое-либо действие генерирует код, в IDE выводится предварительный вариант сгенерированно- го кода. Выберите действие Generate Method, чтобы сгенерировать новый метод с именем SetUpGame. В окне Preview changes выводится описание ошибки, из­за которой появилась красная волнистая линия, а также предваритель- ный вариант кода, предложенного действи- ем для исправления ошибки. Если вы еще не на 100% понимаете, что такое метод, это вполне нормально.
дальше4 63 начинаем программировать на C# Каждый раз, когда вIDE появляется изображение лампочки, это означает, что для выбранного вами кода доступно быстрое действие, а следовательно, существует задача, которую VisualStudio может автоматизировать для вас. Чтобы просмотреть доступные быстрые действия, щелкните на лампочке или нажмите клавиши Alt+Enter или Ctrl+. (точка). Попробу йт е в ыполнит ь код . ЩелкнитенакнопкевверхнейчастиIDE, чтобызапустить программу(какэтобылосделано ранее для консольного приложения). Что-то пошло нетак. Вместо окна программа выдает исключение: Можетпоказаться, что нашапрограмма сломана, но на самом деле произошло именно то, чтодолж- но было произойти!IDEприостанавливаетвашупрограмму ивыделяетпоследнюю выполненную строкукода. Присмотримсяповнимательнее: thr ow n ew Not Impl eme nte dExc ept ion (); Метод, сгенерированныйIDE,прямо приказываетC#выдать исключение. Обратитевниманиена сообщение,выводимоес исключением: внем говорится,чтометодилиоперациянереализованы: System.NotImplementedException: 'The method or operation is not implemented.' И этовполнелогично, потому что вы сами должны реализовать метод, сгенерированныйIDE. Есливы забылиреализовать его, исключение напомнит вам, что у вас осталась невыполненная работа. Если вы генерируетемного методов, такое напоминание будет очень полезным! Щелкните на кнопке Stop Debugging пане ли инструментов(или выберите команду Stop Debugging (F5) в меню Debug), чтобы прерватьработу программы и за- вершить реализацию метода SetUpGame. 3 Кнопка Start Debugging на панели инструментов в верхней части IDE запускает приложение. Также приложение можно запустить командой Start Debugging (F5) из меню Debug. Если вы запускаете приложе- ние кнопкой IDE, кнопка Stop Debugging немедленно завершает е го.
64 глава 1 эмодзи во множественном числе Завершение метода SetUpGame Метод SetUpGame размещается внутри метода public MainWindow(), потому что все содержимое этого метода вызываетсясразу же после запуска приложения. Началодобавления кода в метод SetUpGame. Метод SetUpGame получает восемь пар эмодзи с изображе- нием животныхислучайнымобразомраспределяет ихмежду элементамиTextBlock.Следовательно, первое, чтопонадобит- ся вашемуметоду, — это список этих эмодзи,а IDEпоможет написать длянего код.Выделитекоманду throw, добавленную IDE, и удалите ее.Затемустановите курсор в то место, гдебыла эта команда, ивведите List.IDEоткрывает окноIntelliSense с ключевыми словами,которые начинаютсяс «List»: ВыберитевспискеIntelliSenseстрокуList. Затем введите<str— на экранепоявляетсяещеодно окно IntelliSense с подходящимиключевымисловами: Выберите строку string. Завершите ввод следующей строки кода, но пока не нажимайте кла- вишу Enter: List<string> animalEmoji = new List<string>() 1 Это специальный метод, кото- рый называется конструктором. О том, как он работает, вы узна- ете в главе 5. List — коллекция для хранения набора значений в определенном порядке. Коллекции рассмат­ риваются в главах 8 и 9. Ключевое слово «new» ис- пользуется для создания списка List. О нем вы узнаете в главе 3. Вскоре вы бу- дете знать о ме- тодах гораздо больше. Мы воспользовались IDE для добавления метода в прило- жение, но даже если вы еще не совсем понимаете, что та- кое метод, ничего страшного вэтом нет. В следующей главе вы узнаетемного нового о ме- тодах и о структурекода C#. РЕ Л АКС
дальше4 65 начинаем программировать на C# Добавление значений в List. Команда C# ещене закончена. Убедитесь в том, что курсоррасположенпосле) в конце строки, после чего введите открывающуюфигурную скобку{—IDEавтоматическидобавит парную закры- вающую скобку, а курсорбудет установленмежду двумя скобками. НажмитеEnter — IDE добавит разрывыстрокавтоматически: Используйте панельэмодзиWindows(нажмите клавишуслоготипомWindows+точка)или зайдите на свой любимыйсайтс эмодзи(например,https://emojipedia.org/nature)и скопируйтеотдельный символ эмодзи.Вернитесь ксвоемукоду,введите", вставьте символ, аза ним ещеоднукавычку", запятую,пробел, еще однукавычку«, снова тот же символ эмодзи, последнюю кавычку"изапятую. Потомпроделайте то же самое длясемидругихэмодзи, чтобы витоге вфигурныхскобкахбыли заключены восемь пар эмодзи с изображением животных. Добавьте; после завершающей фигурной скобки: За верш ение мет од а. Теперь добавьте остальной код метода — будьте внимательны с точками, круглымиифигурными скобками: Красная волнистая линия под mainGrid в IDE указывает на ошибку: ваша программа не будет построена,потому что с этим именем в коденичего не связано. Вернитесь кредактору XAML и щелкните на теге <Grid>, затем перейдите к окну свойств и введите mainGrid в поле Name. Проверьте кодXAML—вверхнейчастиразметкисеткинаходитсятег<Grid x:Name="mainGrid">. Сейчас никакихошибоквкоде быть недолжно. Еслионивсеже есть, тщательнопроверьтекаждую строку — совершить ошибку привводеочень легко. 2 3 Задержите указатель мыши на точках под animalEmoji — IDE сооб- щит вам, что присвоенное значение нигде не исполь- зуется. Предупреждение исчезнет сразу же после того, как список эмодзи будет использован далее в коде метода. Панель эмод- зи встроена в Windows 10. Чтобы вы- звать ее на экран, на- жмите кл ав ишу с логот ип ом Windows + точка. Эта строка следует сразу же за закрывающей фигурной скобкой и точкой с запятой. Если при запуске игры произойдет исключение, проверьте, что список animalEmoji содержитровно 8 пар эмодзи, а в коде XAML содержатся 16 тегов <TextBlock.../>. Пока панель эмодзи остается открытой, вы можете ввести слово (например, «octopus»), и оно будет заме- нено соответствующим эмодзи. Не забудьте очистить поле поиска
66 глава 1 построенная программаработает — отличная работа Запуск программы Щелкните на кнопке панели инструментов,чтобы запустить программу.Открываетсяокно с восемью парамиживотных вслучайныхпозициях: Вовремя выполнения программыIDE переходит в режим отладки: кнопка Start заменяется недо- ступной кнопкой Continue, а на панели инструментов появляютсяотладочные элементы ; этикнопки предназначены для приостановки,остановкии перезапуска программы. Остановитесвою программу, щелкнув на кнопке со значкомX в правом верхнем углу окна или на кнопке Stop(кнопка сквадратом) в отладочных элементах. Выполните программу несколькораз — животныебудуткаждыйразнаходиться вразныхпозициях. Припервом запуске программы в верхней части окна появляется панель инструментов времени выполнения: Щелкните на первой кнопке панели, чтобы вызвать панель Live Visual Tree в IDE: Затем щелкните на первой кнопке панели Live Visual Tree, чтобы отключить панель инструментов времени выполнения. Ого, игра уже сейчас неплохо выглядит! Вы подготовили обстановку для следующей части, которую мы добавим в приложе ние . Впроцессепостроенияновой игрывынепростопишете код —вытакже запускаете проект. В самомэффективномвариантереализациипроектстроится небольши- мичастями,и прикаждом изменении вызапускаетепроектиубеждаетесь в том, чторабота движется вправильном направлении.Притакомподходевысможете легко сменить курс,еслипроизойдетчто-то непредвиденное.
дальше4 67 начинаем программировать на C# Поздравляем — высоздалиработающую программу!Разумеется,программированиенесколько сложнеепростого копированиякода изкниги. Но дажеесливы никогда неписаликодпрежде,вас удивит, сколько всего вы уже понимаете. Соедините линией каждую команду C# в левом столбце с описанием того, что делает эта команда, в правом столбце. Мы привели решение для первой команды,чтобы вамбыло проще. Команда C# Что делает Создаетновыйгенератор случайныхчисел Создает списокизвосьми пар эмодзи Выбираетслучайное число от0 до коли- чества эмодзив спискеиназначаетему имя«index» Использует случайное число с именем «index» для получения случайного эмодзи из списка Обновляет TextBlock случай- ным эмодзи из списка Удаляет случайный эмодзи из списка Еще одно упражнение, в котором вам придется поработать карандашом. Не жалейте времени на выполнение всех этих упражнений, потому что они помогут быстрее закрепить важные концепции C# в вашем мозгу. Кто и что делает? ае ааеееееееееееееееее Находит каждыйэлементTextBlock всетке иповто- ряетследующие команды для каждого элемента
68 глава 1 Следующее упражнение поможет вам лучше понять код C#. 1. Возьмите лист бумаги и поверните его набок, чтобы он лежал в альбомной ориента- ции. Нарисуйте вертикальную линию в середине. 2. Запишите весь метод SetUpGame в левой части, оставляя свободное место между командами. (Особая точность с эмодзи не нужна.) 3. В правой части листа запишите каждый из ответов «Что делает» рядом с командой, с которой он соединен. Прочитайте обе стороны — код должен постепенно приоб- ретать смысл. не пропускайте упражнения Команда C# Использует случайное число с именем «index» дляполучения случайного эмодзииз списка Что делает Создает новый генератор случайных чисел Создает списокизвосьми пар эмодзи Выбираетслучайноечисло от0 до ко- личества эмодзив списке иназначает емуимя «index» Находит каждый элемент TextBlock в сетке иповторяетследующиекоманды для каждого э лем ента Обновляет TextBlock случайным эмодзииз списка Удаляетслучайныйэмодзииз списка Кто и что делает? ае ааеееееееееееееееее ре ш ение Возьми в руку карандаш M I N I
дальше4 69 начинаем программировать на C# ¢ Visual Studio — интегрированная среда разработки (IDE) компании Microsoft, которая упрощает редакти- рование файлов с кодом C# и выполнение различных операций с ними. ¢ Консольные приложения .NET Core — кроссплатфор- менные приложения с текстовым вводом и выводом. ¢ Функция IDE IntelliSense помогает быстрее вводить код. ¢ WPF(или Windows Presentation Foundation) — техноло - гия, используемая для построения визуальных прило- жений в C#. ¢ Пользовательские интерфейсыWPF строятся на XAML (eXtensible Application Markup Language) — языке раз- метки на базе XAML, который использует теги и свой- ства для определения элементов управления в пользо- вательском интерфейсе. ¢ Тег Grid в XAML предоставляет структуру сетки, в кото- рой могут содержаться другиеэлементы. ¢ Тег TextBlock вXAML добавляет элемент, который мо- жет содержать текст. ¢ Окно свойств IDE упрощает редактирование свойств элементов управления — например, изменение их структуры, текста или строки/столбца сетки, в котором они находятся. Не уверена, нужны ли все эти упражнения. Разве не лучше просто дать мне код, который можно ввести в IDE? Отработка навыков понимания кода повысит вашу квалификацию разработчика. Письменныеупражнения являютсяобязательными.Онипредостав- ляют вашемумозгуновыйспособусвоенияинформации.Однако они делают нечто еще более важное: предоставляют вам возможность ошибаться. Ошибки — важная часть обучения,и мытожедопустили множество ашибок(возможно, вы даженайдетеодну-две опечатки в этой книге!). Никто не пишет идеальный код с первого раза — действительно хорошие программисты всегда предполагают, что написанный сегодня код, возможно, потребуется изменить завтра. Позднее вкниге выузнаете орефакторинге— приеме программирова- ния,сутью которого становитсяулучшениевашего кода после того, как он будет написан. В таких списках «ключевых момен- тов» приводятся краткие сводки многих идей и средств, которые были представлены в этой главе. КЛЮЧЕВЫЕ МОМЕНТЫ
70 глава 1 контролируйте свой код Добавление нового проекта в систему управления версиями Вэтой книге мыбудем строить многоразныхпроектов. Толькопред- ставьте, какудобнобылобыиметьместо, вкотором ихможнобыло бысохранить или загружатьпозднеес любогоустройства?А если вы допустите ошибку,разве небудет удобно вернуться к предыду- щей версии вашего кода? Что ж, вам повезло! Именно эту задачу решаетсистемауправления версиями:она предоставляетпростые средствадлясозданиярезервнойкопиивсего кода иотслеживания всехвносимыхизменений.VisualStudioпозволяет легкодобавлять проекты всистемууправленияверсиями. Git — популярная система управления версиями, и VisualStudio мо- жет публиковать ваш исходный код влюбомрепозиторииGit. Мы считаем GitHub одним из самых удобных провайдеровGit. Для со- хранениякодавампонадобитсяучетнаязаписьGitHub.Еслиувасеще нет учетнойзаписи, зайдите на сайт https://github.comисоздайте ее. Найдите элемент Addto SourceControl встрокесостояния вниж- нейчастиIDE: Щелкнитенанем — VisualStudio предлагаетдобавить кодвGit: Выберите строку Git. Visual Studio запрашивает имя и адрес электронной почты. Строка состояния после этого должна вы- гляд еть та к: Теперь ваш код находится под контролем системы управления версиями. Задержитеуказатель мыши над : IDE сообщает, что на вашем компьютере остаются две сохранен- ные версии вашего кода, которые небыли сохранены в сетевом хранилище. Привключениивашего проекта в системууправления версиямиIDE открывает окно TeamExplorer на одной панели сSolution Explorer. (Есливыневидите этоокно,выберите его вменюView.)ОкноTeamExplorerпредназначено длявзаимодействия ссистемой управления версиями. С егопомощью высможете опубликоватьсвойпроект вудаленном репозитории. Когда накомпьютерепоявляютсялокальныеизменения, окно Team Explorerиспользуется дляотправки их в удаленныйрепозиторий. Для этого щелкните на кнопке Publish to GitHub в окнеTeam Explorer. Включать проект в систему управ- ления версиями необязательно. Возможно, выработаетенакомпью- тереофиснойсети,котораянеимеет доступа к GitHub(рекомендованно- му нами провайдеру Git). А может быть, вам простоне хочется это де- лать. Как бы то ни было, этот шаг можнопропустить —или же опубли- ковать код в приватном репозито- рии, если вы хотитехранитьрезерв- нуюкопию,нонехотите,чтобыона была доступной для других. Как тольковы включите свой код в Git, строка состояния изменяет- ся и пок азывает, что код проекта н ах од ится под к он тролем сис темы управления версиями. Git — очень популярная система управления версиями, и в Visual Studio включен полнофункциональный клиент Git. Впапке проекта создается скрытая папк а с именем .git, которая исполь- зуется Gi t для отслежив ан ия в сех изменений, вносимых в код. После создания учет- ной запись на github. com щелкните на этой кнопке, чтобы опублико- вать свой код на GitHub. РЕ Л АКС
дальше4 71 начинаем программировать на C# Когда вы нажимаетекнопку Publish to GitHub, Visual Studio открывает форму входа GitHub. Введите имя пользователя и пароль GitHub. (Есливы настроили двухфакторную аутенти- фикацию,вам такжебудетпредложено исполь- з ова ть ее.) После того как IDE выполнит вход в GitHub, открывается формаPublish to GitHub. На ней можновыбратьпровайдераGitHub, имяпользо- вателяи имяпроекта, атакже указать, должен ли репозиторийбыть приватным. Оставьте параме- трам значенияпоумолчанию. Нажмитекнопку Publish, чтобы опубликовать проект вGitHub. ПослепубликациивGitHub статусGit встроке состояния обновляется; теперь строка состоя- ния сообщает, что несохраненных изменений нет. Это означает,что вашпроектсинхронизи- ровансрепозиториемвучетнойзаписиGitHub. После того как код будет опу- бликован в GitHub, вы сможете использовать Team Explorer для работы с репозиторием Git. Git предоставляет мощные средства командной строки. Visual Studio может установить их для вас автомати- чески. Visual Studio упрощает работу с Git, и вы може- те установить эти средства, но это необязательно. После выполнения входа в GitHub эта кноп- ка публикует проект в разделе вашей учет- ной записи. Чтобы просмотреть только что сохраненный код, откройте страницу https://github.com/<ваше-имя-пользователя-github>/MatchGame. Когда вы синхронизируете свой проект с удаленнымрепозиторием, обновления появляются в разделе Commits. Убедитесь в том, что здесь отображается ваше имя пользователя GitHub. Git — система управления версиями, распространяе- мая с открытым кодом. Существует много сторонних сервисов, предоставляющих услуги Git (таких, как GitHub): выделение пространства для хранения кода, веб-доступ к вашим репозиториям и т. д. Чтобы боль- ше узнать оGit, зайдите на сайтhttps://git-scm.com .
72 глава 1 xaml — код пользовательского интерфейса В:XAML действительно является кодом? О:.Да, безусловно. Помните красную волнистую линию, которая появилась под mainGrid в коде C# и исчезла только при вклю- чении имени в тег Grid в XAML? Это объясняется тем, что мы ужеизменяем код — после добавления имени вXAML он может использоваться вашим кодомC#. В: Ясчитал, что XAML — что -то вродеразметкиHTML, интер- претируемойбраузером. Это не так? О:Нет, XAML — это код, которыйстроится параллельно с вашим кодомC#.В следующей главе вы узнаете о том, как использовать ключевое словоpartial дляразбиения класса на несколькофайлов. Именно так происходит соединение XAML с C#: XAML опреде- ляет пользовательский интерфейс, C# определяет поведение, и они объединяются при помощи разделяемых (partial) классов. Вотпочему такважнорассматриватьXAMLкаккод,ахорошеезнание XAMLстановится важнымнавыком для любогоразработчикаC#. В: ЯзаметилМНОГО строк using в началефайлаC#.Почему их так много? О:ПриложенияWPF обычно используют кодизразных пространств имен (о том, что такое пространство имен, вы узнаете в следующей главе.) Когда средаVisualStudio создает проект WPF за вас, она ав - томатическивключаетв началофайлаMainWindow.xaml.cs директивы using для самых распространенных пространств. Собственно, вы ужеприменяетенекоторыеиз них: IDE используетболеесветлыйцвет символов для обозначения пространств имен, неиспользуемых в коде. В: Похоже, настольные приложения намного сложнее кон- сольных. Они действительноработают одинаково? О:Да.Еслиразобраться,весь кодC#работаетпоодному принципу: выполняется одна команда, потом другая, затем следующая и т. д. Настольныеприложения кажутся намного более сложными из-за того, что некоторые методы вызываются только при выполнении определенных условий — скажем, при появлении окна на экране или нажатии кнопки пользователем. После того как метод будет вызван, дальше онработает потем жеправилам, что иконсольное прило жение. Посмотрите нанижнюючастьредакторакода— вней сейчасвыводитсясообщение . Это означа- ет, что построение программы проходит успешно, это тот процесс, который используетсяIDEдляпреобразования кодав двоичный формат, который может выполнятьсявашей операционной системой. Давайте в порядке экспе- римента сломаем программу. Перейдите впервуюстрокукодав методе SetUpGame. Нажмите клавишуEnter дважды, после чего введите вотдель- ной строке символы Xyz. Сновапроверьтесообщение в нижней части редакторакода —теперьв немвыводится уведомление . Если увас в IDEне открыто окно Error List, откройте его командой Error List из менюView. В окне Error List выводятся опи- сания трехошибок: IDE выводит эти ошибки, потомучто строка Xyz не является допустимым кодомC#, и это непозволяетIDEпостроить вашкод. Покав коде остаются ошибки, он выполняться неможет; удалите добавленнуюстрокуXyz. Подсказка для IDE: список ошибок часто Зада вае мы е вопросы
дальше4 73 начинаем программировать на C# Следующий шаг построения игры ¦ обработка щелчков Итак,игра выводитживотных, на которых долженщелкать пользователь. Теперь нужно добавить код, обеспечивающийработусамой игры. Игрокщелкает наживотных, составляющихпару.Первое животное, накоторомбыл сделанщелчок,исчезает. Есливтороеживотное,на котором щелкнетигрок,совпадает с первым, то оно тожеисчезает. Если нет, первое животноепоявляется снова. Чтобы эта схемаработа- ла, мы добавим обработчик события, т. е. метод, вызываемый при выполнении некоторых операций (щелчков, двойных щелчков,измененияразмеровокна и т. д.)вприложении. TextBlock_M ouseDown() { /* Если щелчок сделан на первом * животном в паре, сохранить * информацию о том, на каком * элементе TextBlock щелкнул * пользователь, и убрать животное * с экрана. Если это второе * животное в паре, либо убрать * его с экрана (если животные * составляют пару), либо вернуть * на экран первое животное (если * животные разные). */ } Когда игрок щелкает на одном из живот- ных, приложение вызывает метод с име- нем TextBlock_MouseDown для обработки щелчка. Вот что делает этот метод: Это комментарий. Весь текст между /* и */ игнорируется в C#. Мы добави- ли комментарий, чтобы объяснить, что должен делать метод TextBlock_MouseDown, а также чтобы показать, как выглядят комментарии. Вы здесь! MainWindow.xaml.cs MainWindow.xaml Создание проекта Конструиро- вание оКна НаписаНие кода C# обработка щелчков Добавление та йм ера
74 глава 1 вернемся к игре Реакция TextBlock на щелчки НашметодSetUpGame настраиваетэлементыTextBlockтак, чтобывнихвыводились эмодзис изображе- ниемживотных. Этот пример показывает,какваш кодможет изменять элементы вприложении. Атеперь необходимо написать код,который идетв другомнаправлении — ваши элементы должны обращаться с вызовамик вашему коду, иIDE поможет вам в этом. Вернитесь кредакторуXAMLищелкнитена первомтегеTextBlock —IDEвыделит этот тег вдизайнере, чтобы вы могли отредактировать его свойства. Затем перейдитев окно свойстви щелкните на кнопке EventHandlers ( ).Обработчиком события называетсяметод,которыйвызывается привозникнове- нииконкретногособытия. Кчислутакихсобытийотносятсянажатия клавиш,перетаскиваниемышью, изменение размеровокна и, конечно, перемещения указателямышиищелчки.Прокрутитеокносвойств и просмотрите имена различных событий, для которых кTextBlock можно добавлять обработчики со- бытий. Сделайте двойной щелчок на поле справа от события MouseDown. IDE автоматически заполнила поле MouseDown именем метода TextBlock_ MouseDown,а вкодеXAML для TextBlock появляется свойствоMouseDown: <TextBlock Text="?" FontSize="36" HorizontalAlignment="Center" VerticalAlignment="Center" MouseDown="TextBlock_MouseDown"/> Возможно, выэтогоне заметили, потому чтосредаIDEтакжедобавила новый метод вкодпрограммнойчасти(код,связанныйсXAML)инемедленно пере- ключилась на редактор C#, чтобы его вывести. Вы всегда можете вернуться обратно из редактораXAML; для этого щелкните правой кнопкой мыши на методе TextBlock_MouseDown в редакторе XAML и выберите команду View Code. Добавленный метод выглядит так: Каждыйраз,когда игрокщелкаетнаэлементеTextBlock,приложение автома- тическивызывает методTextBlock_MouseDown.Таким образом, остаетсялишь добавить внегокод. Затем остаетсясвязать все остальные элементыTextBlock с этим методом, чтобы онитоже вызывалиего. Обработчик событий – метод, который вызывается вашим приложением в ответ на такие события, как щелчок кнопкой мыши, нажатие клавиши, изменение размеров окна и т. д . Эти кнопки переключают окно свойств между выво- дом свойств и выводом обработчиков событий. дважды щелкните здесь Щелкните на верхнем элементе TextBlock в вашем коде XAML, чтобы выделить его в окне конс тр укто ра .
дальше4 75 начинаем программировать на C# Нижеприведенкод методаTextBlock_MouseDown.Преждечем добавлятьэтот код в программу,прочитайте егоипопробуйте понять,чтоон делает. Неогор- чайтесь, есликакие-то предположения оказались ошибочными!Наша цель — постепеннонаучить ваш мозг читатькод C# ипонимать егосмысл. TextBlock lastTextBlockClicked; bool findingMatch = false; private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e) { TextBlock textBlock = sender as TextBlock; if (findingMatch == false) { textBlock.Visibility = Visibility.Hidden; lastTextBlockClicked = textBlock; findingMatch = true; } else if (textBlock.Text == lastTextBlockClicked.Text) { textBlock.Visibility = Visibility.Hidden; findingMatch = false; } else { lastTextBlockClicked.Visibility = Visibility.Visible; findingMatch = false; } } 1.Чтоделает findingMatch? 2.Чтоделает блок кода,начинающийся сif (findingMatch == false)? 3. Чтоделает блок кода,начинающийся сelse if (textBlock.Text == lastTextBlockClicked.Text)? 4.Что делает блок кода,начинающийся сelse? Возьми в руку карандаш
76 глава 1 учимся понимать код Ниже приведен код метода TextBlock_MouseDown. Прежде чем до- бавлять этот код в программу,прочитайте егои попробуйте понять, чтоонделает.Не огорчайтесь,если какие-топредположения оказа- лись ошибочными! Наша цель — постепенно научить ваш мозг чи- тать код C# ипонимать егосмысл. TextBlock lastTextBlockClicked; bool findingMatch = false; private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e) { TextBlock textBlock = sender as TextBlock; if (findingMatch == false) { textBlock.Visibility = Visibility.Hidden; lastTextBlockClicked = textBlock; findingMatch = true; } else if (textBlock.Text == lastTextBlockClicked.Text) { textBlock.Visibility = Visibility.Hidden; findingMatch = false; } else { lastTextBlockClicked.Visibility = Visibility.Visible; findingMatch = false; } } А вотчто делает весь код метода TextBlock_MouseDown. Чтение кода на новом языке программирования напоминает чтение нотной записи — это навык, который развивается тренировкой. Чем больше вы будете этим заниматься, тем лучше у вас будет получаться. Возьми в руку карандаш Решение 1.Чтоделает findingMatch? Этот признак определяет, щелкнул ли игрок на первом животном в паре, и теперь пы- тается найти для него пару. 2.Чтоделает блоккода,начинающийся сif (findingMatch == false)? Игрок только что щелкнул на первом животном в паре, поэтому это животное ста- новится невидимым, а соответствующий элемент TextBlock сохраняется на случай, если его придется делать видимым снова. 3.Чтоделает блоккода,начинающийсяс else if (textBlock.Text == lastTextBlockClicked.Text)? Игрок нашел пару! Второе животное в паре становится невидимым (а при дальнейших щелчках на нем ничего не происходит), а признак findingMatch сбрасывается, чтобы сле- дующее животное, на котором щелкнет игрок, снова считалось первым в паре. 4.Чтоделает блок кода,начинающийся сelse? Игрок щелкнул на животном, которое не совпадает с первым, поэтому первое выбран- ное животное снова становится видимым, а признак findingMatch сбрасывается.
дальше4 77 начинаем программировать на C# Добавление кода TextBlock_MouseDown Теперь вы примерно представляете,как работаеткодTextBlock_MouseDown, имы перейдем к добавле- нию его в программу.Вотчто мы сделаем дальше: 1. Вставьте первые две строки с lastTextBlockClicked и findingMatch перед первой строкой метода TextBlock_MouseDown, добавленного IDE. Проследите за тем, чтобы они были вставлены между закрывающейфигурной скобкойвконце SetUpGame иновымкодом,добавленнымIDE. 2. ЗаполнитекодTextBlock_MouseDown. Будьте внимательны со знакамиравенства:=принципиально отличается от == (об этом выузнаетевследующейглаве). А вот как это выглядит в IDE: Это поля — переменные, находящиеся внутри класса, но за пределами любых методов, чтобы они были доступными длявсех методов окна. Поля более подробно рассматриваются в главе 3. { IDE выводит над методом TextBlock_MouseDown подсказку «1 reference», потому что сейчас один элемент TextBlock связан с его событиемMouseDown.
78 глава 1 обработка событий мыши Вызов обработчика события MouseDown остальными элементами TextBlock На данный момент только первый элементTextBlock связан со своим событием MouseDown. Теперь необходимо проделать то же самое для остальных 15 элементовTextBlock.Для этого можно выбрать каждыйэлементввизуальном конструктореиввестиTextBlock_MouseDownвполерядом сMouseDown. Выуже знаете,как добавить свойство вкод XAML; давайтевоспользуемся этим приемом. Ваш проект достиг важной контрольной точки! Возможно, игра еще не закончена, но она ужеработает, поэтому сейчас будет уместно ненадолго задержаться и подумать над тем, как бы улучшить ее. Какие изменения вы бы п ред ложили для того, чтоб ы сделать п рог рамму более ин тересн ой? Запуститепрограмму ипопробуйтещелкать напарахживотных, чтобыониисчезали.Первоеживотное исчезает в любом случае. Если потом вы щелкнете на совпадающем животном, то оно тоже исчезнет. Еслижевторой щелчок будетсделан на неподходящем животном,то первое животноеснова появится на экране. Когда всеживотные будутскрыты,перезапустите или закройтепрограмму. Выделите остальные 15 элементов TextBlock вредакторе XAML. ПерейдитевредакторXAML,щелкнитеслева от второго тегаTextBlockипротащитеуказатель мышиповсемостальным элементамTextBlockдозакрывающеготега </Grid>. Врезультатеувас должны быть выделены последние 15 элементов TextBlock (но непервый). 1 Воспользуйтесь заменой для добавления обработчиков событий MouseDown. ВыберитекомандуFindandReplace >> QuickReplaceв менюEdit. Введитеискомый текст/> изамените его текстомMouseDown="TextBlock_MouseDown"/> —проследите за тем, чтобыперед MouseDown находился пробел, а в разделедиапазона поиска было выбранозначениеSelection, чтобы свойство добавлялось только к выделенным элементам TextBlock. 2 Выполните замену во всех 15 выделенных элементах TextBlock. ЩелкнитенакнопкеReplaceAll( ), чтобы добавить свойство MouseDown к элементам TextBlock, — I D E должна сообщить о выполнении 15 замен. Внимательно просмотрите код XAMLиубедитесь в том, что каждый элемент содержит свойство MouseDown, полностью со- впадающеес одноименным свойством первого элементаTextBlock. Проверьте, что над методом теперь выводится подсказка «16 references» (выберите команду BuildSolution из меню Build, чтобыобновить ее).Есливсообщениибудутупомянуты17ссылок, вероятно, выслучайноприсоединили обработчиксобытиякGrid.Этогобыть недолжно —в про- тивном случае прищелчке на животном будетпроисходить исключение. 3 Перед MouseDown ставится про- бел, чтобы избежать случайного слияния с предыдущим свойством. Когда вы встречае- те врезку «Мозговой штурм», как следует поразмыслите над за- данным вопросом. Мозговой штурм
дальше4 79 начинаем программировать на C# Т и к Т и к Тик Таймер срабатывает после истечения заданного интервала, а назначенный метод вызывается снова и снова. Мы используем таймер, который запускается вместе с запуском игры и перестает работать после нахождения последнего животного. Добавление таймера Нашаигра станетболее интересной, еслиигроксможет попытать- сяпобить свойрекорд. Добавим таймер,который срабатываетс фиксированным интервалом и многократно вызывает метод. Сделаем игру чуть более азартной! В нижней части окна выводится время, прошедшее с момента запуска игры. По- казания таймера постоянно увеличива- ются, а останавливается таймер толь- ко после нахождения последней пары. Вы здесь! MainWindow.xaml.cs MainWindow.xaml Создание проекта Конструиро- вание оКна НаписаНие кода C# ОбрабОтка щелчкОв добавление та йм ера
80 глава 1 последние штрихи Добавление таймера в код игры Сначала найдите ключевое слово namespace в верхней частиMainWindow.xaml.cs и добавьте прямо под ним строку using System.Windows.Threading;: namespace MatchGame { using System.Windows.Threading; Найдите строку public partial class MainWindow и добавьте следующий код сразу же за от- крывающей фигурной скобкой {: public partial class MainWindow : Window { DispatcherTimer timer = new DispatcherTimer(); int tenthsOfSecondsElapsed; int matchesFound; Таймеру необходимо сообщить, с какой частотой он должен срабатывать и какой метод должен вызываться. Щелкните вначале строки, вкоторойвызывается методSetUpGame, чтобы перевести курсор вэтупозицию. НажмитеклавишуEnterивведитедве строки на следующем снимкеэкрана, начинающиесяс timer., — как только вы введете+=, IDE выведетсообщение: Нажмитеклавишу Tab. IDE завершает строку кода и добавляет метод Timer_Tick: 1 2 3 4 Когда вы нажимаете клавишу Tab, IDE автоматически вставляет метод, ко- торый должен вызываться таймером. Добавьте! Добавьте эти три строки кода, чтобы создать новый таймер и добавить два поля для отслеживания прошедшего времени и количества найден- ных совпадений. Затем добавьте эти две строки. Начните с ввода второй строки: "timer.Tick+=" Как только вы введете знак равен- ства, IDE выведет сообщение "Press TAB to insert".
дальше4 81 начинаем программировать на C# МетодTimer_TickобновляетэлементTextBlock,распростра- няющийсянавсю нижнююстрокусетки. Чтобысоздать этот э лем ент, вып о лните сл едую щие дейс твия: Ì Перетащите элементTextBlockв левый нижний квадрат. Ì ВполеNameвверхнейчастиокна свойстввведите имя timeTextBlock. Ì Сбросьте отступы, выровняйте текст по центру(Center) ячейки, задай- те свойствуFontSizeзначение36px, асвойствуText —значение «Elapsed time» (так же, как это делалось для других элементов управления). Ì Найдите свойствоColumnSpan изадайтеемузначение4. Ì Добавьте обработчик события MouseDown с именем TimeTextBlock_ MouseDown. Код XAMLдолженвыглядеть так(внимательно сравнитеего с кодом вIDE): <TextBlock x:Name="timeTextBlock" Text="Elapsed time" FontSize="36" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Row="4" Grid.ColumnSpan="4" MouseDown="TimeTextBlock_MouseDown"/> Когда вы добавляете обработчик событияMouseDown, VisualStudio создает в коде программной частиметодс именем TimeTextBlock_MouseDown, похожийна другиеэлементыTextBlock.Добавьте в него следующий код: private void TimeTextBlock_MouseDown(object sender, MouseButtonEventArgs e) { if (matchesFound == 8) { SetUpGame(); } } Теперьу вас есть всенеобходимое для завершения метода Timer_Tick, который обновляет новый элемент TextBlock истекшим временем и останавливает таймер после того,как игрок найдет все совпадения: private void Timer_Tick(object sender, EventArgs e) { tenthsOfSecondsElapsed++; timeTextBlock.Text = (tenthsOfSecondsElapsed / 10F).ToString("0.0s"); if (matchesFound == 8) { timer.Stop(); timeTextBlock.Text = timeTextBlock.Text + " - Play again?"; } } Ивсежездесь что-то нетак . Запуститепрограмму... стоп! Происходит исключение. Мыисправим этуошибку, носначала присмотритесь повнимательнее к сообщению об ошибке и выде- леннойстрокевIDE. Авыдогадаетесь, из-за чего возниклаошибка? 5 6 7 Ой-ой! Как выдумаете, чтоздесьпроизошло? Свойство ColumnSpan находится разделе Layout окна свойств. Используйте кноп- ки в верхней части окна для переключе- ния между выводом свойств и событий. Сбрасывает игру, если были найдены все 8 пар (в противном случае ничего не делает, потому что игра еще продолжается).
82 глава 1 пошаговое выполнение кода Диагностика ошибок в отладчике Принц ипы о тла дки Перезапустите свою игру несколько раз. Первое, на что следует обратить внимание, — ва ша программа всегда выдаетисключение одного типа с одинаковым сообщением: Еслиубратьокноисключения,выувидите, чтовыполнение всегда прерывается воднойстроке: Это исключение является воспроизводимым: вы можете стабильнозаставитьвашупрограммувыдаватьоднои тоже исключение, а также достаточно хорошо представляете, где кроется источник проблемы. 1 Исключения используются в C# для передачи информации о том, что во время выполнения кода что-то пошло не так. Каждое исключение относится к определенному типу: это конкретное исключение имеет тип ArgumentOutOfRangeException. Исключениятакже сопровождаются полезными сообщениями, которые помогут вам понять, что же именно произошло в программе. В сообщении нашего исключения сказано: «Индекс вышел за пределы диапазона». Воспользуемся этой информациейдля поиска ошибки. Когда в программе происходит исключение, часто это ста- новится хорошей новостью — в программе обнаружилась ошибка, и теперь вы можете заняться ее исправлением. У каждой ошибкиесть объяснение — в программе ничего не происходит без при- чин, но не каждую ошибку легко обнаружить. Пониманиеошибки — первый шаг к ее исправлению. К счастью,для этого существует превосходныйинструмент — отладчик VisualStudio. Стр ока , в которой выдается исключе- ние .
дальше4 83 начинаем программировать на C# Когда ваше приложение приостанавливается в отладчике (это называется прерыванием), на панели инструментов появляются элементы отладки. В книге вы еще не раз по- тренируетесь в их использовании, поэтому запоминать их назначение сейчас не обязательно. Пока просто прочитай- те описания и наведите указатель мыши на каждый эле- мент, чтобы увидеть название и эквивалентное сочетание к л авиш. Эта кнопка продолжает выполнение приложения. Если вы нажмете ее сей- час, то снова будет вы- дано то же исключение. Кнопка Break All приостанавливает при- ложение. Если приложение уже приоста- новлено, кнопка недоступна. Кнопка Stop Debugging уже использовалась для приостановки прило- жения. Кнопка Restart перезапу- скает приложение. Факти- чески приложение оста- навливается и запускается заново. Кнопка Show Next Statement переводит кур- сор к следующей команде, которая должна быть вы- полнена в программе. Кнопка Step Into вы- полняет следующую команду. Если эта команда является вызовом метода, то выполняется толь- ко первая команда в этом методе. Кнопка Step Over также выполняет следующую коман­ ду, но если эта команда является вызовом метода, то будет выполнен весь метод в целом. Кнопка Step Out завер- шает выполнение те- кущего метода и пре- рывает программу в строке, следующей за той, из которой он был вызван. Добавьте точку прерывания в строке, в которой выдается исключение. Сновазапуститепрограмму, чтобыонабыла прервана при выдачеисключения. Прежде чем оста- навливать ее, выберитекоманду ToggleBreakpoint (F9)изменюDebug.Как только выэто сдела- ете,строкабудет выделена красным цветом, а слева отнеепоявитсякрасная точка. Теперь снова остановите приложение — выделениеи точка останутся на своем месте: Вытолькочтоустановили встрокеточку прерывания. Ваша программабудет останавливаться каждыйраз, когда она будет достигать этой строки. Убедитесь в этом: снова запустите свое при- ложение. Программа остановится в этой строке, но на этот раз исключение не выдается. На- жмите кнопку Continue. Программа снова останавливается в той же строке. НажмитеContinue ещераз. Программа снова останавливается.Продолжайте,пока непоявитсяисключение.Теперь остановитеприложение. 2 Снова запустите приложение, но на этот раз наблюдайте за происходящим бо- лее внимательно.Ответьте на следующиевопросы. 1. Сколькоразостанавливалось приложениеперед исключением? ____________________________________ 2. Вовремя отладкиприложения появляетсяокноLocals.Каквыдумаете,чтооноделает? (ЕслиокноLocals не появляется на экране,выберите в меню командуDebug>> Windows >>Locals(Ctrl D,L). Анатомия отладчика Возьми в руку карандаш
84 глава 1 вы знаете мои методы, Ватсон Разновидность цикла, выполняемого для каждого элемента в коллекции. Циклыпредназначеныдлямногократноговыполненияблокакода.Внашем коде используется циклforeach, или особый вид цикла, который выполняет один и тот же код для каждого эле- мента вколлекции (такой, как списокanimalEmoji). Примериспользованияциклаforeach со списком чисел: List<int> numbers = new List<int>() { 2, 5, 9, 11 }; foreach (int aNumber in numbers) { Console.WriteLine("The number is " + aNumber); } Этотцикл foreachсоздаетновуюпеременнуюсименем aNumber.Затем онпоследовательноперебираетэлементы спискаnumber ивыполняет Console.WriteLine для каждогоэлемента, присваивая aNumber очередноезначение из спискаList: The number is 2 The number is 5 The number is 9 The number is 11 Здесь мыпредставляем новуюконцепцию— нотольковобщихчертах, чтобы выхотябыпримернопонимали,как работаеткод.Циклыбудутнамногоподробнеерассмотрены вглаве2.Затемв главе3 мывернемсяк циклам foreach инапишем цикл, который имеет многообщегосприведенным вышециклом.Даже если сейчасвам кажется,что мыдвижемсяслишком быстро,к моментувозвращенияк примеру вглаве 3всестанет намногоболеепонятным.По собственномуопыту мызнаем, чтоповторноечтение кода, когдау васпоявился болееширокийконтекст,сильно помогает закрепитьновые знания в мозгу... так чтонеогорчайтесь, еслисейчасчто-токажется слегка туманным. Ваше приложение останавливалось 17 раз. После 17­го раза было выдано исключение. В окне Locals выводятся текущие значения переменных и полей. В нем вы можете отслеживать изменения этих значений во время выполнения программы. Цикл foreach выполняет команду Console.WriteLine для каждого числа в списке. Цикл foreach снова и снова выполняет один коддля каждого элемента коллекции, при этом переменной каждый раз присваивается следующий элемент. Таким образом, в данном случае переменной aNumber присваива- ется следующее число из списка, после чего эта переменная используется для вывода строки текста. З а с ц е н о й Соберите факты, которые помогут вам разобраться в причинах проблемы. Вызаметиличто-нибудь интересное в окнеLocals при запуске приложения?Перезапустите его и пристально следите за переменной animalEmoji. Когда ваше приложение прервется в первый раз,вокне Locals должна выводиться следующая информация: Нажмитекнопку Continue. Похоже, значение Count уменьшается на 1, с 16 до 15: Приложение добавляет случайные эмодзи из списка animalEmoji в элементы TextBlock, а затем удаляет их из списка, так что значение Count должно каждыйразуменьшаться на1. Все идет заме- чательно, покасписокanimalEmojiнеостанетсяпустым(такчтоCount содержит0);вэтот момент происходит исключение. Первыйфактнайден!Другойфакт — все это происходит вциклеforeach. Ипоследнийфактзаключаетсявтом,чтовсе началосьпосле добавленияновогоэлементаTextBlockвокно. Пришло время побывать вроли Шерлока Холмса. А вы сможете выследить, что же становится причиной исключения? 3 Возьми в руку карандаш Решение
дальше4 85 начинаем программировать на C# Ис правл ение ошиб ки. Таккакисключениевыдаетсяиз-за того, что в программе кончаютсяэмодзи в цикле, перебирающем элементыTextBlock, ошибкуможно исправить, пропускаявцикле последнийдобавленныйэлемент TextBlock.Для этого можно проверить имяTextBlock и пропустить элемент для вывода времени. Удалите точку прерывания, повторно переключив ее состояние или выбравкоманду Delete All Breakpoints (Ctrl+Shift+F9) из меню Debug. foreach (TextBlo ck textBlock in mainGrid. Children.OfType<TextBlock >()) { if (textBlock.Name != "timeTextBlock") { textBlock.Visibility = Visibility.Visible; int inde x = random.Next(animalEmo ji.Count); string n extEmoji = animalEmoji[in dex]; textBloc k.Text = nextEmoji; animalEm oji.RemoveAt(index); } } 5 Это не единственный способ исправления ошибки. Одна из истин, которые вы узнаете после того, как напишете много программ, — что у любой задачи существует много, ОЧЕНЬ много решений... и эта ошибка не яв- ляется исключением (простите за каламбур). Добавьте эту команду if в цикл foreach, чтобы она пропускала элемент TextBlock с именем timeTextBlock. Поискфактической причины ошибки. Ошибка в программе происходит из-за того, что она пытается получить следу- ющий эмодзи из списка animalEmoji, но списокпуст. Из-за этого и происходит исключение ArgumentOutofRange. Из-за чего же кончились эмодзи? Довнесенияпоследнего изменения программа работала. Затем мы добавили TextBlock... и программаработать перестала. Ошибка возникает в цикле, пере- бирающемвсе элементыTextBlock.Наводитна размышления... очень,очень интересно. Итак,когда вы запускаетесвоеприложение,оно прерывается в этой строкедля каждого элемента TextBlock вокне. Для первых16 элементовTextBlock всепроходитнормально,потомучто вколлек- ции содержитсядостаточно эмодзи: Но после появления нового элемента TextBlock в нижней части окна прерывание происходит в 17-йраз, а посколькуколлекция animalEmojiсодержала всего16 эмодзи,она пуста: Итак, передвнесениемизменения увасбыло16элементовTextBlockисписокиз16эмодзи;на каж- дыйэлементTextBlockприходилось по одномуэмодзи. Теперь вколлекции17 TextBlock,а эмодзи только 16,поэтому программа ненаходит эмодзи для добавленияв окно... и выдает исключение. 4 Отладчик выделяет команду, ко- торая должна быть выполнена. Так выглядит программа непо- средственно перед тем, как в ней будет выдано исключение. По с леду Добавьте этот код, чтобы ис- править ошибку.
86 глава 1 вы отлично справились Чтобы просмотреть и загрузить полный код этого проекта, а также всех остальных проектов в книге, перейдите поадресу https://gitgub.com/head-first-csharp/fourth-edition/ Добавьте оставшийся код и завершите построение игры Осталось решить еще одну проблему. Метод TimeTextBlock_MouseDown проверяет поле matchesFound, но это поленигдене инициализируется. Добавьтеследующие тристроки вметод SetUpGameнепосред- ственно послезакрывающейфигурнойскобки цикла foreach: animalEmoji.RemoveAt(index); } } timer.Start(); tenthsOfSecondsElapsed = 0; matchesFound = 0; } Затем добавьте следующую команду в средний блок if/else в TextBlock_MouseDown: else if (textBlock.Text == lastTextBlockClicked.Text) { matchesFound++; textBlock.Visibility = Visibility.Hidden; findingMatch = false; } Теперь в игреработает таймер, который останавливается после того, как игрок завершит поиск пар, а после завершения игры вы можете щелкнуть на нем, чтобы сыграть снова. Вы построили свою первую игру на C#. Поздравляем! Теперь в вашей игре рабо- тает таймер, который отсчитывает время, необ- ходимое игроку для нахож- дения всех пар. Сможете ли вы побить свой рекорд? Добавьте эти три строки в конец мето- да SetUpGame, чтобы запустить таймер и сбросить содержимое полей. Добавьте эту строку, чтобы зна- чение matchesFound увеличивалось с каждой успешно найденной парой.
дальше4 87 начинаем программировать на C# Выберите команду CommitStaged andSync из раскрывающегося списка (он находится прямо подполем сообщенияо сохранении).Синхронизацияможетзанять несколько секунд,послечего в окнеTeam Explorerпоявитсясообщение об успехе: 3 Да, это очень удобно — разбить игру на меньшие задачи, которые можно решать поочередно. Любой крупный проект всегда рекомендуется разбивать на меньшие части. Один из самых полезных навыков программирования — умение взглянуть набольшую и сложную задачу иразбить ее наряд мень- ших,легко решаемых задач. Всамомначалебольшого проекта легковпасть вуныние:«Ого, да это бесконечнаяработа!»Но есливы сможетевыделить меньшую подзадачу и начнете трудиться над ней, это станет отправной точкой для работы. А когда эта часть будет завершена, можно перейтик следующей меньшей части,потом к следующей,и т. д. Вовремяпостроениякаждойчастивыбудете все большеузнавать о проектев целом. Нажмитекнопку +, чтобы проиндексировать файлы, — тем самымвысообщаетеGit,чтофайлы готовыксохранению.Если вывнесетеизменения вфайлыпослетого, каконибылипроиндексиро- ваны, вудаленномрепозиториибудут сохранены только проиндексированные изменения. 2 Введите сообщение с крат- кимописанием изменений. 1 Обновление кода в системе управления версиями Итак, ваша игра успешно работает. Сейчас самый под- ходящий момент для сохранения изменений в Git, и в Visual Studio это делается очень просто. От вас по- требуется лишь проиндексировать изменения, ввести сообщение о сохранении, а затем синхронизировать проект судаленным репозиторием. Сохранять ваш код в репозитории Git необязательно — но это опред еленно стоит делать!
88 глава 1 любую игру можно улучшить ¢ Visual Studio отслеживает количество ссылок на метод в кодеC# илиXAML. ¢ Обработчик события представляет собой метод, который вызывается вашим приложением при воз- никновении некоторого события (щелчка кнопкой мыши, нажатия клавиши , изменения размеров окнаит.д.). ¢ IDE упрощает добавление методов обработчиков событий и управление ими. ¢ В окне Error List в IDE выводятся описания оши- бок, которые препятствуют построению вашего приложения. ¢ Таймеры многократно выполняют обработчик со- бытияTick с заданным интервалом. ¢ foreach — разновидность циклов для перебора коллекций элементов. ¢ Когда в вашей программе происходит исключение, соберите информацию и постарайтесь опреде- лить, что является его причиной. ¢ Воспроизводимые исключения прощеустранять. ¢ Visual Studio упрощает использование систем управления версиями для резервного копирова- ния кода и контроля вносимых вами изменений. ¢ Вы можете сохранять свой код в удаленном репо- зиторииGit.Мы создалирепозиторий с исходным кодом всех проектов в книге на GitHub. Еще лучше, если... Игра получилась вполне достойной!Но любую игру— да,собственно,практически любуюпрограмму— можно усовершенствовать.Несколько предложений, которые, как нам кажется,моглибыулучшить нашу игру: Ì Добавьтебольше видовживотных, чтобывигре неиспользовались одни ите жеизображения. Ì Следите за лучшим временем игрока,чтобы он мог попытаться побить свойрекорд. Ì Реализуйте обратный отсчет времени, чтобы времяигрокабыло ограничено. Асможетели вы предложить собственные улучшения для игры? Это очень полезное упражнение — подумайте несколько ми- ну т и запишите не менее трех улучшений дляигры по поиску пар. О т л и чная раб о т а ! Мы абсолютно серьезно — не жалейте времени и сделайте это. Когда вы дела- ете шаг назад и размышляете о только что завершенном проекте, это отлично помогает закрепить в мозгу только что усвоенный материал. На всякий случай напомним: в книге мы часто называем Visual Studio просто «IDE». Возьми в руку карандаш M I N I КЛЮЧЕВЫЕ МОМЕНТЫ
Погружение в C# 2 Команды, классы и код Вы не просто пользователь IDE. Вы — разработчик. IDE может сделать за вас очень многое, и все же ее возможности не без- граничны. Visual Studio — одна из самых совершенных систем разработки программного обеспечения, однако мощная IDE — только начало . Пришло время заняться углубленным изучением кода C#: какую структуру он имеет, как он работает, как управлять им... Потому что нет предела тому, что вы можете делать в ваших приложениях. (И для ясности: вы можете быть настоящим разработчиком независимо от того, какие клавиатуры вы предпочитаете. Требование только одно: пи- сать качественный код!) Я слышала, что настоящие разработчики используют только механические клавиатуры «со щелчком». Это правда?
90 глава 2 команды в методах, методы в классах Присмотримся к файлам консольного приложения В последней главе мы создали проект консольного приложения .NET Core и присвоили ему имя MyFirstConsoleApp.Когда вы это сделали,среда Visual Studio создала две папкии трифайла. MyFirstConsoleApp.sln MyFirstConsoleApp.csproj Program.cs MyFirstConsoleApp MyFirstConsoleApp АтеперьрассмотримфайлProgram.cs . Откройтеего вVisual Studio: Ì В верхней частифайларазмещаетсядиректива using.Подобные строкиusing присутствуют во всехфайлах с кодом C#. Ì Сразу же после директив using идет ключевое слово namespace. Ваш код принадлежит про- странствуимен с именемMyFirstConsoleApp.Сразу жепосленегоследует открывающаяфигурная скобка {, а в конце файла — закрывающая фигурнаяскобка }. Все, что находится между этими скобками, принадлежитпространствуимен. Ì Внутри пространства именнаходится класс. В вашейпрограмме используется один класс с име- нем Program. Сразу же за объявлением класса следует открывающая фигурнаяскобка, а парная закрывающая скобка располагаетсяв предпоследнейстрокефайла. Ì Внутри класса находится метод с именем Main — за объявлением также следует пара фигурных скобок с содержимым. Ì Методсодержит одну команду: Console.WriteLine("Hello World!"); Среда Visual Studio создала за вас две папки и три файла. Этот файл со- держит код, который вы запускали. Метод с именем Main. При запуске консольное приложение ищет класс, содержащий метод с именем Main, и начинает выполнение с первой команды этого метода. Метод называется точкой входа, потому что именно в этом методе C# «входит» в программу. Снимок экрана сде- лан в Visual Studio для Windows. Если вы работаете в macOS, экран будет выгля- деть немного иначе, но код останется тем же.
дальше4 91 близкое знакомство с C# Namespace Class Method 1 statement statement Method 2 statement statement В:Я понимаю, для чего нужен файл Program.cs — здесь хранится код моей программы. Но зачем нужны два другихфайла и папки? О: Когда вы начинаете новый проект вVisual Studio, среда создает решение (solution). Решение представляет собой контейнер дляпроекта. Файл решения имеет суффикс .sln и содержит список проектов, входящих в решение, а также незначительный объем дополнительной информации(например, версию Visual Studio, которая использовалась для создания решения). Проект хранится в папке внутри папки решения. Для проекта выделяется отдельная папка, потому что некоторыерешения могут содержать несколько проектов, но в нашем решении проект только один и его имя совпадает с именемрешения(MyFirstConsoleApp).Папка проекта вашего приложения содержитдвафайла:файлProgram.cs с кодомифайл проектас именемMyFirstConsoleApp.csproj.Файл проекта содержит всю информацию, необходимую Visual Studioдля построения кода, т. е. преобразования его вформу, которая может выполняться на компьютере. В будущем вы увидите в папке проекта ещедве папки: папка bin/ содержит используемые файлы, построенные на основе вашего кодаC#, а в папке objхранятся временные файлы, использованные при построении. При создании классов для них определяются пространства имен, чтобы эти классы существовали отдельно от классов, поставляемых с .NET. Класс содержит один или несколько методов. Методы всегда должны принадлежать классу. Методы состоят из команд (таких, как команда Console.WriteLine, используемая приложением для вывода строки на консоль). Порядок следования методов в файле класса роли не играет. Метод 2 с таким же успехом может размещаться перед методом 1. Команда выполняет одно действие Каждыйметодсостоит из команд, такихкаккоманда Console.WriteLine. Когда ваша программа вызывает метод, она выполняет первую команду, затемследующую, затемследующую ит. д.Когда будет выполнена последняякомандаметода (или программадостигнет командыreturn), методзавершаетсяи выполнение программы продолжается с точкипослекоманды,изкоторойбыл вызванметод. Класс содержит часть вашей программы (хотя очень маленькие программы могут состоять из одного класса). Код всех программ C# имеет одинаковую структуру. Во всех программах используются пространства имен, классы и методы, чтобы с кодом было удобнее работать. Анатомия отладчика часто Задава ем ые вопросы
92 глава 2 namespace PetFiler2 { public class Fish { public void Swim() { // команды } } public partial class Cat { public void Purr() { // statements } } } MoreClasses.cs SomeClasses.cs na mesp ace Pe tFil er2 { public class Dog { public void Bark() { // здесь размещаются команды } } public partial class Cat { public void Meow() { // другие команды } } } Взглянитенафайлыс кодом C#из программыс именем PetFiler2.Онисодержат трикласса:Dog, Cat иFish.Так как всеонипринадлежатодному пространству именPetFiler2, команды метода Dog.Bark смогут вызывать методы Cat.Meow и Fish.Swim без включения директивы using. Два класса могут находиться в одном пространстве имен (и файле!) Если метод помечен ключевым словом public, это означает, что он может использоваться другими классами. Класс может быть разбит поразным файлам только при использовании ключевого слова partial. Возможно, в коде, написанном для этой книги, эта возможность будет использоваться не так часто, но она еще встретится вам в этой главе, и мы хотим избежать неприятных сюрпризов. Класс может охватывать несколько файлов, но при его объявлении должно использо- ваться ключевое слово partial. Неважно, какразные пространства имен иклассырас- пределяются по файлам. Привыполнении ониработаютточно так же. разделяемые классы
дальше4 93 близкое знакомство с C# IDE помогает разработчику написать правильный код. Давным-давно программистам приходилось вводить код в простых текстовыхредакторах, таких как Блокнот для Windows илиTextEdit для macOS.В товремя некоторыевоз- можности таких редакторовбыли невероятно передовыми (например,поискс заменой илиCtrl+Gдляпереходакстроке с заданным номеромвБлокноте).Намприходилось использо- вать много хитроумныхприложенийкоманднойстрокидля построения,запуска,отладкии развертываниякода. Запрошедшиегоды Microsoft(ибудемоткровенными— мно- гиедругиекомпании,а такжеотдельныеразработчики)при- думала много других полезных функций: выделение ошибок, IntelliSense,визуальноередактированиепользовательского интерфейса врежимеWYSIWYG,автоматическоегенериро- ваниекода и т. д. Послемногихлет эволюции среда Visual Studio стала одной изсамых совершенныхсистемредактирования кода. Иксча- стью для нас, она также стала отличным инструментом для изучения C# и исследования процесса разработки приложений. Выходит, IDE действительно сильно упрощаетработу. Среда и генерирует код, и помогает мне с поиском проблем в моем коде.
94 глава 2 программы состоят из команд В: Я уже видел фразу «Hello World». У нее есть какой-то осо- бый смысл? О: «HelloWorld» — программа, которая делает только одно: она выводитфразу «HelloWorld»,чтобыпоказать,что программадействи- тельно успешно запускается и выполняется. Часто она становится первойпрограммой, которую выпишетенановомязыке,адля многих из нас — первым кодом, который мы пишем на любом языке. В: Фигурных скобок очень много — в них трудно ориентиро- ваться. Без нихдействительно не обойтись? О:ВC#фигурныескобки(некоторыелюдиназываютих «усами»,но мы этот термин не используем) группируют команды внутриблоков. Фигурные скобки всегда образуют пары. Закрывающая фигурная скобка может встретиться в программе только после открывающей. IDE помогает находить парные фигурные скобки — щелкните на одной скобке, и она вместе со своей парной скобкой изменит цвет. Такжеможносворачивать/разворачивать блоки в фигурных скобках при помощи кнопки в левой части редактора. В:Что жетакое «пространства имен» и для чего они нужны? О:Пространства имен помогаютупорядочитьразличные средства, необходимые вашей программе. Чтобы вывести строку текста, при- ложение использует классConsole, являющийся частью.NETCore — кроссплатформенного фреймворка с открытым кодом, который содержитмногочисленныевспомогательные классыдля построения п рилож ений. И « многоч ис ленные» сле дует п онимать бу кв аль но — эт и классы исчисляются тысячами, поэтому .NETиспользует пространства имендля ихупорядочения.КлассConsoleпринадлежит пространству имен с именем System, поэтому чтобы вы могли использовать его в программе, в начале кода должнарасполагаться директива using System;. В:Яплохопонимаю, что такое точкавхода. Можете объяснить еще раз? О: Вашапрограмма состоит из множества команд, и эти команды немогут выполняться одновременно.Программа начинает с первой команды,выполняет ее,переходит кследующей,затемк следующей за ней и т. д.Команды обычнораспределяются по классам. Итак,вы запускаете свою программу.Как ейузнать, с какой команды нужно начать выполнение? Именно для этого и нужна точка входа. Чтобы ваш кодуспешнопостроился, в нем должен присутствовать ровно один метод с именем Main. Он называется точкой входа, потому что программа начинает выполнение (т. е. входит в код) cпервой команды методаMain. В: Значит, мои консольные приложения .NET Core будут ра- ботатьв другихоперационных системах? О:Да! .NETCoreявляется кроссплатформеннойреализацией .NET (который включает такие классы, как List и Random), так что ваши приложения могут запускаться на любом компьютере с системой Windows, macOS илиLinux. Попробуйтесделать это прямо сейчас. Вам понадобится .NETCore. Программа установки Visual Studio устанавливает .NET Core автоматически, но .NET Core также можно загрузить по адресу https ://dotnet.m ic ros oft.c om/d ownl oad. Послетогокакподдержка.NETCoreбудетустановлена, найдитепапку проекта —щелкните правой кнопкой мыши на проектеMyFirstConsoleApp в IDE и выберите команду OpenFolderinFileExplorer (Windows) или RevealinFinder(macOS). Перейдите в соответствующий подкаталог bin/Debug/ ископируйтевсефайлы накомпьютер, на которомдолжна выполнятьсяпрограмма. После этого программу можнобудетзапустить, и онабудетработать налюбом компьютере с системойWindows, Mac или Linux сустановленной средой .NETCore: В: Обычно я запускаю программы двойным щелчком, но с файлом .dllуменяничегонеполучается. Возможно ли создать обычный исполняемый файл Windows или macOS, который можно запустить напрямую? О:Да.Командаdotnetпозволяетпубликовать исполняемыедвоич- ныефайлыдлядругих платформ.Откройте окнокомандной строки или терминал, перейдитев папку, в которой находится файл .sln или .csproj, и выполните эту команду,чтобы сгенерировать исполняемый файлWindows, — э тот приемработаетв любойоперационнойсистеме с у ст анов ленной команд ой dotnet, не толь ко в W indo ws : dotnet publish -c Release -r win10-x64 Последняя строка вывода должна содержать текст MyFirstCon- soleApp-> , за которым следует имя папки. Папка содержит файл MyFirstConsoleApp.exe (и несколькоDLL-файлов, необходимых для егоработы).Такжеможностроить исполняемыепрограммыдлядругих платформ. Замените win10-x64 на osx-x64, чтобы опубликовать автономное приложениедля macOS: dotnet publish -c Release -r osx -x64 или укажите linux-x64 для публикации приложения Linux. Этот параметр называется идентификатором среды выполнения (RID)— полный списокRIDдоступен по адресу https://docs.microsoft. c om/en- us /dotnet/c or e/ri d-c atal og. Этот снимок экрана сделан в macOS, но в Windows команда dotnet работает точно так же. Задаваемые вопросы часто
дальше4 95 близкое знакомство с C# Команды являются структурными элементами приложений Вашеприложение состоит из классов,классы содержат методы, а методы состоят изкоманд.А значит,есливыхотитепостроить приложение,котороерешаетмного разныхзадач, вам понадобится многоразных видов команд. Однуразновидность командвыужевидели: Console.WriteLine("Hello World!"); Этакомандавызываетметод, а именнометод Console.WriteLine, которыйвыводит строкутекста на консоль. В этой ив другихглавахкнигибудутописаны другиераз- новидностикоманд. Несколько примеров: Переменные и объявления переменных позволяют приложению хранить данные и работать с ними. Во многих программах задействованы математические вычисления, поэтому математические операторы используются для вычитания, умножения, деления и т. д . Условные команды позволяют нам выбирать между вариантами, чтобы в программе выполнялся либо один блок кода, либо другой. Благодаря циклам один блок программы выполняется снова и снова, пока не будет выполнено некоторое условие.
96 глава 2 Объявление переменных Приобъявлениипеременнойвысообщаете программе ее типиимя. Зная тип вашейпеременной,C#сможетвыдавать ошибки при попыткевыпол- нения бессмысленных операций — например, при попытке вычитания "Fido" из 48353. Такие ошибки препятствуют построению программы. Несколькопримеров объявленияпеременных: // Let's declare some variables int maxWeight; string message; bool boxCheck ed; Переменные используются в программах для работы с данными Любая программа, независимо от ее размера, работает с данными. Эти данные могутбыть представлены в форме документа, картинки в видео- игре, обновления в социальной сети,и всеравно они остаются данными. Здесь-то в игруивступают переменные. Онииспользуются программой для хранения данных. Переменные могут изменяться В разныемоментывыполненияпрограммы переменная может содержать разные значения. Другимисловами, значение переменной изменяется (соб- ственно,поэтомуони и называются«переменными»).Иэтоочень важныймо- мент, потомучтоэта идеялежитв основе любой написанной вами программы. Допустим, ваша программа присваивает переменнойmyHeightзначение 63: int myHeight = 63; Каждый раз, когда myHeight встречается в коде, C# заменяет ее имя теку- щим значением 63. Допустим, позднее переменнойбудет присвоено новое значение 12: myHeight = 12; Сэтогомомента C#будет заменять myHeightзначением 12(покапеременная снова не изменится),но переменнаяпо-прежнемубудет называтьсяmyHeight. Каждый раз, когда вашей программе требуется работать с числами, текстом, значениями «ис- тина/ложь» или любыми другими видами данных, для хранения этих дан- ных используются переменные. Типы переменных. Для C# типопределяет, какие данные могут храниться в переменной. Имена переменных. С точки зрения C# неважно, какие имена присвоены переменным, они нужны толькодля вас. Вот почему так важно выбирать содержатель- ные и понятные имена переменных. Любая строка, начинающаяся с//, является комментарием и не выполняется программой. Комментарии можно использовать для добавления примечаний, которые помогут людям разобраться в вашем коде и понять его логику. у каждой переменной есть тип
дальше4 97 близкое знакомство с C# пе-ре-мен-ная, сущ. элемент или фактор, который с большой веро- ятностью может измениться. Если вы пишете код с использованием переменных, которым не было присвоено значение, ваш код строиться не будет. Этой ошибки можно легко избежать, для этого достаточно объединить объявление переменной с присваиванием в одну команду. Перед использованием переменной необходимо присвоить значение Попробуйте ввести следующие команды не- посредственно перед командой вывода «Hello World» вновом консольном приложении: string z; string message = "The answer is " + z; Попробуйтесделать это прямосейчас. Выполучите сообщение об ошибке, и IDE не сможет построить ваш код. Дело втом, что IDEпроверяет каждую пере- меннуюи следитза тем, чтобыперед использованием ей было присвоено значение. Чтобы вы не забыли присвоить значениепеременнойпередееиспользо- ванием, проще всего объединить объявление пере- меннойс командой,присваивающейей значение: int maxW eight = 25000; string message = "Hi!"; bool boxCheck ed = tru e; После того как переменной будет присвоено значение, это значение можно в любой момент изменить. А зна- чит, присваивание перемен- ной исходного значения при объявлении не создает ника- ких неудобств. Несколько полезных типов Укаждой переменной имеется тип, который сообщает C#,какого рода данныевней могутхраниться.Различные типы C#будутподробнорассмотрены вглаве4,а пока мы сосредоточимсяна трехсамыхпопулярныхтипах.Типint предназначен для хранения целых чисел, в типе string хранитсятекст, а втипе bool — логические значения «ис- тина/ложь». Эти значения присваиваются переменным. Вы можете объявить переменную и присвоить ей начальное значение в одной команде (хотя это и необязательно). Сделайте э то!
98 глава 2 начин аем пи сать ко д Генерирование нового метода для работы с переменными Впредыдущей главе выузнали, что VisualStudioумеет генерировать код за вас. Этовесьмаудобно,когда вы пишетекод,а такжеможет бытьочень полезнымучеб- ным средством. Давайтевоспользуемсятем,чтовы узналиранее,иприсмотримся повнимательнеек генерированию методов. Добавьте метод в новый проект MyFirstConsoleApp. Откройте проект консольного приложения, созданный в последней главе. IDE создала ваше приложениес методом Main, которыйсодержитровно одну команду: Console.WriteLine("Hello World!"); Замените ее командой, в которой вызывается другой метод: OperatorExamples(); Получите информацию о проблеме от Visual Studio. Кактольковызавершите замену,VisualStudio подчеркнет вызовметода красной волнистой линией. Наведитена нее указатель мыши. В IDEпоявляетсявременноеокно с подсказкой: Visual Studio сообщает вам две вещи: что в программе существует проблема — вы пытаетесь вы- звать несуществующий метод (из-за чего построение кода невозможно) и чтоу среды имеются предложенияпо ее исправлению. Сгенерируйте метод OperatorExamples. ВсистемеWindows временное окно предлагает нажать Alt+EnterилиCtrl+для просмотравозмож- ныхисправлений.ВmacOSприсутствует ссылка «Showpotentialfixes» — нажмитеOption+Return. Нажмитеодну из этих комбинаций клавиш(или щелкнитена раскрывающемся списке слева от временного окна). У IDE есть решение: она сгенерируетметод с именем OperatorExamples вклассе Program. Щелк­ ните на ссылке PreviewChanges, чтобы открыть окно с потенциальным решением IDE — до- бавлениемнового метода. Затем щелкните на кнопке Apply,чтобы включить метод в ваш код. 1 2 3 Когда IDE генерирует новый метод за вас, она включает команду throw как временный заполнитель — при запуске ваша программа прервет- ся, как только она достигнет этой команды. Команду throw необходимо заменить кодом. Снимок экрана сделанв Windows. Он несколько от- личается от версии для Mac, но содержит ту же информацию. На Mac щелкните на ссылке или нажмите Option+Return, чтобы вывести список пред- лагаемых решений. Сделайте э то!
дальше4 99 близкое знакомство с C# Добавление кода с использованием операторов Итак,впеременнойхранятсяданные.Что теперь снимиможносделать?Если это число, его можно с чем-нибудь сложить или умножить. Если это строка, ееможно объединить с другимистроками. В этом вам помогутоператоры. Нижеприведенотело нового мето- да OperatorExample. Включите этот код в своюпрограмму и прочитайтекомментарии, чтобыузнать об использованныхв нем операторах. private static void OperatorExamples() { // Эта команда объявляет переменную и присваивает ей значение 3 int width = 3; // Оператор ++ инкрементирует переменную (увеличивает ее на 1) wi dth ++; // Объявляем еще две переменные int для хранения чисел // и используем операторы + и * для сложения и умножения значений intheight=2+4; int area = width * height; Co nso le.W rit eLi ne(a rea); // Следующие две команды объявляют строковые переменные // и объединяют их оператором + (эта операция называется конкатенацией) string result = "The area"; result=result+"is"+area; Co nso le.W rit eLi ne(r esu lt) ; // Логическая переменная может содержать // либо true, либо false bool truthValue = true; Co nso le.W rit eLi ne(t rut hVa lue) ; } Команды,толькочтодобавленные вваш код,выводятнаконсольтристроки:каждаякоманда Console.WriteLine выводит отдельную строку.Прежде чем запускать программу, постарайтесь разобраться, что будет выведено, и запишите результаты. Ине пытайтесь подсмотреть решение — его здесь нет! Чтобы проверить свои ответы, просто запустите программу. Подсказка: припреобразованииboolвстрокуможетбытьполученрезультатFalseилиTrue. Строка1: Строка2: Строка3: Строковые переменные используют- ся для хранения текста. Когда вы ис- пользуете оператор + со строками, эти строки объединяются, так что сложение "abc"+"def" дает строку "abcdef". Такое объ- единение строк называется конкатенацией. M I N IВозьми в руку карандаш
100 глава 2 отладчик помогает понять код Использование отладчика для наблюдения за изменением переменных Когда вызапускалисвою программуранее,она выполнялась вотладчике—невероятно полезноминстру- менте, которыйпомогаетпонять, какработают программы.Выможете использовать точкипрерывания для приостановки программы в тот момент, когда она достигает определенных команд, и следить за значениямипеременных в окне просмотра. Воспользуемся отладчиком,чтобыувидеть кодв действии. Нам потребуются трифункции отладчика, которые вы найдете на панели инструментов: Еслипрограмма окажетсяв состоянии,которого вы неожидали, просто перезапустите отладчик кнопкойRestart . Установите точку прерывания и запустите программу. Наведитеуказатель мыши на вызовметода, добавленныйк методу Mainвашейпрограммы,ивы­ беритекоманду ToggleBreakpoint (F9)изменюDebug.Строка должна выглядеть так: Нажмите кнопку , чтобы выполнить программу вотладчике,как это делалось ранее. Выполните программу в пошаговом режиме с входом в метод. Вашотладчикостанавливаетсявточкепрерывания,установленнойна командес вызовом метода OperatorExamples. Нажмитекнопку Step Into (F11)— отладчикзаходитв метод, а затем останавливается перед вы- полнением первойкоманды. Проверьте значение переменной width. При пошаговом выполнении кода отладчик приостанавливается после каждой выполненной команды. Разработчик получаетвозможность проверить значения переменных. Наведите указа- тель мыши на переменную width. IDE открывает временное окно с текущим значением переменной — в настоящее время оно равно 0. Теперь нажмите кнопкуStepOver(F10)—управлениепропускаеткомментарий и переходитк первой команде,которая выделяется цветом. Мы хотим выполнить команду, поэтому снова нажмите кнопку StepOver(F10).Еще раз задержитеуказатель мышина width.Теперь переменнаясодержитзначение3. 1 2 3 Выделенная фигурная скобка и стрелка на левом поле означают, что код был приостановлен перед вы- полнением первой команды ме тод а. На Mac для управления отладкой используются комбинации клавиш Step Over( O),StepInto( I)иStepOut ( U). Экраны будут незначительно отличаться, но отладчик работает точно так же, как показано в «Руководстве для пользователя Mac» (приложение 1). Сделайте э то!
дальше 4 101 близкое знакомство с C# В окне Locals выводятся значения переменных. Объявленные переменные являются локальными по отношению кметодуOperatorExamples — этоозначает, чтоонисуществуют только внутри методаи могут выполнятьсякомандами в методе. Впроцессе отладки VisualStudio выводит значенияпеременныхв окнеLocals в нижней частиIDE. Добавьте отслеживание для переменной height. Однойиз самых полезных возможностейотладчика являетсяокно Watch,котороеобычнорас- полагаетсяна тойже панели, что иокноLocals внижнейчастиIDE.Когдавынаводите указатель мышина переменную, вы можетедобавить еедля отслеживания — щелкнитеправойкнопкой мышина имени переменнойво временном окнеи выберитекомандуAdd Watch. Переменная height появляется в окне Watch. Выполните остальной код метода в пошаговом режиме. Выполнитевпошаговомрежиме каждую команду OperatorExamples. В ходе пошагового выпол- ненияметода обращайте внимание на окнаLocals иWatch иследитеза изменением значений. В системе Windows нажмите Alt+Tab до и после команд Console.WriteLine, чтобы переключаться на отладочную консоль иобратно для просмотра вывода. ВmacOS выводнаправляется вокно терминала,так что вам не придетсяпереключатьсямеждуокнами. 4 5 6 Отладчик – один из самых полезных инструментов Visual Studio, а заодно и прекрасный инструмент для анализа работы ваших программ. Окна Locals и Watch в Visual Studio для Mac выглядят не- много иначе, чем в Windows, но содержат ту же инфор- мацию. Отслеживаемые значения в версиях Visual Studio для Windows и Mac добавляются одним и тем же способом.
102 глава 2 = против== Использование операторов для работы с переменными Хорошо, данные сохранены в переменной. Что с ними можно сделать? Часто в программе требуется выполнить некоторые действия в зависимости от значения. И здесь начинают играть важную роль операторы проверкиравенства, операторыотношения илогические операторы: Операторы для сравнения двух переменных int Простые проверки проверяют значение переменной при помощи оператора сравнения. Примеры сравнения двух переменных int, x и y: x < y (меньше) x > y (больше) x == y (равно – обратите внимание на два знака равенства) Именно такие проверки вы будете использовать чаще всего. Логич ес кие о пера т оры Отдельныеусловия объединяютсяводно составноеусловиепри помощи оператора&&(И)иоператора||(ИЛИ). Вот как можно проверить, что i равно3 или j меньше 5: (i==3)||(j<5) Операторы отношения Операторы > и< используются длясравнения чисел: онипрове- ряют,что число в одной переменнойбольше или меньшечисла в другой переменной. Также можно использовать оператор >= для проверки того, что одно значение больше либо равно другому,илиоператор <= для проверки того,что одно значениеменьшелиборавно другому. О пе рат оры прове рки раве нст ва Оператор == сравнивает два значения и возвращает результат true,еслиони равны. Оператор!= очень похож на ==, но он возвращаетtrue,если два сравниваемыхзначениянеравны. Не путайте операторы = и ==! Оператор = (один знак равенства) присваива- ет значение переменной, а оператор == (два знака равенства) проверяет два значения на равенство. Вы не поверите, сколько оши- бок в программах — даже уопытных программи- стов! — встречается из-за использования = вместо ==. Если IDE начинает жаловаться на то, что «не удается преобразовать тип 'int' в 'bool'», то , скорее всего, именно это произо- шло в вашей программе. Будьте осторожны!
дальше 4 103 близкое знакомство с C# Принятие решений в командах if Командыif ввашей программе выполняютнекоторые действиятолько в томслучае, если заданныеусловия истинны(илиложны).Команда ifпроверяетусловиеивыполняет код, еслипроверка дает результат true.Очень частокомандыifпроверяют два значенияна равенство. В таких ситуациях используется оператор ==(напомним: он отличается от оператора =, которыйиспользуетсядляприсваивания). int som eVal ue = 10; string message = ""; if (someValue == 24) { message = "Yes, it's 24!"; } Команды if/else также выполняют некоторые действия, если условие не истинно Командыif/elseрасширяют логикуif:еслиусловие истинно, ониделают что-тоодно, аеслинет — что -то другое. Командаif/elseпредставляет собойкомандуif,закоторой следуют ключевое слово else и второй набор выполняемых команд. Еслиусловиеис- тинно,программа выполняеткоманды впервойпарефигурныхскобок,а еслинет — вы- полняются командыиз второйпары. if (someValue == 24) { // Фигурные скобки могут содержать // сколько угодно команд message = "The value was 24."; } els e { message = "The value wasn't 24."; } Каждая команда if начинается с условия в круглых скобках. Затем следует блок команд в фигурных скобках, который выполняется в случае истинности условия. Команды в фигурных скобках выполняются только в том случае, если условие истинно. ПОМНИТЕ: для проверки равенства двух значений всегда используются два знака равенства.
104 глава 2 циклы и условия Умногихпрограмм(иособенноигр!)естьоднастраннаяособенность: они почтивсегда требуют повторения некоторых действий. Для этого в программах используются ци­ клы — они приказывают вашей программе выполнять заданный набор команд, пока некотороеусловиеостается истинным(илиложным). Циклы while выполняются, пока условие остается истинным Вциклах whileвсекоманды вфигурныхскобкахвыполняются,пока условиевкруглых скобкахостается истинным. while (x > 5) { // Команды в этих фигурных скобках выполняются, // пока значение x больше 5 } Циклы do/while сначала выполняют команды, а потом проверяют условие Циклdo/while очень похож на цикл while, но с одним отличием. Цикл while сначала проверяетусловие,а затем выполняет своикоманды только втом случае,еслиусловие истинно. Таким образом,циклdo/whileхорошо подходит для тех случаев, вкоторых набор команддолженбыть гарантированно выполненхотябы одинраз. do { // Команды в фигурных скобках будут выполнены один раз, // а затем цикл будет продолжаться, пока // выполняется условие x > 5 }while(x>5); Циклы for выполняют свой блок при каждом проходе Циклfor выполняет команду прикаждом выполнении (итерации). for(inti=0;i<8;i=i+2) { // Все команды, заключенные в фигурные // скобки, будут выполнены 4 раза } Циклы выполняют некоторые действия снова и снова Части заголовка for называются инициализатором (int i=0), условием цикла (i<8) и итератором (i=i+2). Каждый проход цикла for (ивообще любого цикла) называется итерацией. Условие цикла всегда проверяется в начале каждой итерации, а итератор всегда выполняется в конце итерации. Заголовок цикла for состоит из трех команд. Первая команда задает начальное состояние переменной цикла. Цикл продолжает выполняться, пока вторая команда остается истинной. Наконец, третья команда продолжает выполняться после каждого прохода цикла.
дальше 4 105 близкое знакомство с C# Ниже приведены примеры циклов. Запишите, будет ли каждый цикл повторять- сябесконечноили со временемзавершится. Если он завершится, то сколько раз онбудет выполнен? Также ответьте навопросы в комментарияхв циклах2 и 3. // Цикл #1 int count = 5; while (count > 0) { count = count * 3; count = count * -1; } // Цикл #4 inti=0; int count = 2; while(i==0){ count = count * 3; count = count * -1; } // Цикл #2 intj=2; for(inti=1;i<100; i=i*2) { j=j -1; while (j < 25) { // Сколько раз будет // вып олн ена // сле дую щая ком анд а? j=j+5; } } // Цикл #3 intp=2; for(intq=2;q<32; q=q*2) { while (p < q) { // Сколько раз будет // вы полн ена сл едую щая // ко манд а? p=p*2; } q=p -q; } // Цикл #5 while (true) { int i = 1;} Помните: цикл for всегда проверяет условие перед выполнением блока, а итератор — в конце блока. Подсказка: значение p изна- чально равно 2. Подумайте над тем, когда будет выполняться итератор "p = p * 2". Циклы for устроены сложнее (но при этом более гибки) по сравнению с простыми циклами while или do. Самаярас- пространенная разновидность цикла for просто выполняется заданное количество раз. Фрагмент кода for застав- ляет IDE создать такую разновидность цикла for: Цикл for состоит из четырех частей: инициализатора, условия, итератора и тела: for (инициализатор; условие; итератор) { те ло } В большинстве случаев инициализатор используется для объявления новой переменной — например, инициали- затор int i = 0 в приведенном фрагменте кода for объявляет переменную с именем i, которая может использо- ваться только внутри цикла for. После этого цикл выполняет тело (это может быть одна команда или блок команд в фигурных скобках), пока условие остается истинным. В конце каждой итерации цикл for выполняет итератор. Таким образом, следующий цикл for: for(inti=0;i<10;i++){ Console.WriteLine("Iteration #" + i); } будет выполнен 10 раз, а на консоль будут выведены сообщения Iteration #0, Iteration #1, ... , I teration #9. Циклы for под увеличительным стеклом Когда вы используете фрагмент for, нажа - тие Tab используется для переключения между i и length. Если вы измените имя переменной i, фрагмент автоматически изменит два других вхождения этого имени. Возьми в руку карандаш
106 глава 2 анализ циклов в отладчике Ниже приведены примеры циклов. Запишите, будет ли каждый цикл повторяться бесконечно или со временем завершится. Если он завер- шится,то сколько раз он будет выполнен? Также ответьте на вопросы в комментариях в циклах2 и3. // Цикл #1 int count = 5; while (count > 0) { count = count * 3; count = count * -1; } Цикл 1 будет выполнен один раз. Помните: count = count * 3 умножает count на 3, после чего сохраняет результат (15) в той же переменной count. // Цикл #4 inti=0; int count = 2; while(i==0){ count = count * 3; count = count * -1; } Цикл 4 будет выполнять- ся бесконечно. // Цикл #2 intj=2; for(inti=1;i<100; i=i*2) { j=j-1; while (j < 25) { // Сколько раз будет // вып олн ена сле дующ ая // команда? j=j+5; } } Цикл 2 будет выполнен 7 раз. Команда j = j + 5 выполняется 6 раз. // Цикл #3 intp=2; for(intq=2;q<32; q=q*2) { while (p < q) { // Сколько раз будет // выполнена следующая // к ома нда? p=p*2; } q=p-q; } Цикл 3 выполняется 8 раз. Команда p = p * 2 выполня- ется 3 раза. // Loop #5 while (true) { int i = 1;} Цикл 5 также является беско- нечным. Не жалейте времени и постарайтесь по-настоящему разобраться в том, как работает цикл 3. Это идеальная возможность опробовать отладчик на практике! Установите точку прерывания на команде q = p - q;, после чего воспользуйтесь окном Locals для наблюдения за изменениями p и q при пошаговом выполнении цикла. Когда мы включаем в кни- гу письменное упражнение, решение обычно приводится на следующей станице. Возьми в руку карандаш Решение
дальше 4 107 близкое знакомство с C# Используйте фрагменты кода для написания циклов Вэтойкнигевыбудете частописать циклы, иVisualStudioможетускорить этурабо- ту при помощифрагментов(snippets)— простыхшаблонов,используемыхдлядо- бавлениякода.Воспользуемсяфрагментами длявключениянесколькихциклов в методOperatorExamples. Есликод все еще выполняется, выберите команду Stop Debugging(Shift+F5)изменю Debug(илина- жмите кнопку Stop на панели инструментов). Затем найдите Console.WriteLine(area); в методе OperatorExamples. Щелкните в конце строки, чтобы курсорбылустановлен после точки с запятой, послечего несколько раз нажмитеEnter,чтобыдобавить немногосвободного места. Теперь начинайте вводить фрагмент. Введите while и дважды нажмите клавишу Tab. IDE добавляетв код шаблон для цикла while,в котором выделено условие: Введите area < 50 – IDE заменит true введенным текстом. Нажмите Enter, чтобы завершить фрагмент. Добавьте между фигурными скобками две команды: while (area < 50) { hei ght ++; area = width * height; } Затем воспользуйтесь фрагментом цикла do/while и добавьте другой цикл сразу же после только что добавленного цикла while.Введитеdo идважды нажмите клавишу Tab.IDEдобавляетфрагмент: Введите area > 25 и нажмитеEnter, чтобы завершить фрагмент. Добавьте междуфигурнымискобками две команды: do { wid th- -; area = width * height; } while (area > 25); Теперь воспользуйтесь отладчиком,чтобы хорошоразобратьсявтом,как работают циклы: 1. Щелкните настроке непосредственно перед первым цикломи выберите командуToggleBreakpoint (F9)изменюDebug,чтобыдобавить точку прерывания. ЗапуститесвойкодинажмитеF5, чтобы перейтик новой точке прерывания. 2. Выполните два цикла в пошаговом режиме при помощи кнопки Step Over (F10). Следите за из- менением значенийheight,width и area вокне Locals. 3. Остановите программу и изменитеусловие цикла while на area < 20, чтобы оба цикла имели ложные условия. Снова проведите отладку программы. Цикл while сначала проверяетусловие ипропускаетцикл,а цикл do/while выполняется однократно, после чего проверяетсяусловие. Еслив программе существуют непарные фигурные скобки, программа строиться не будет. К счастью, IDE поможет вам в этом! Установите курсор у фигурной скобки, и IDE автомати- чески выделит ее пару. Подсказка для IDE: скобки Сделайте это!
108 глава 2 тренировка в использовании циклов Немного потренируемся в работе сусловными командами и циклами. Обновите метод Main в консольном приложении, чтобы он соответствовал новому мето- ду Main, приведенному ниже, после чего добавьте методы TryAnIf, TryAnIfElse и TrySomeLoops. Прежде чем запускать код, попробуйте ответить на вопросы. Затем выполните код и проверьте свои ответы. static void Main(string[] args) { Tr yAn If( ); Tr ySo meL oops (); Tr yAn IfE lse( ); } private static void TryAnIf() { int someValue = 4; string name = "Bobbo Jr."; if ((someValue == 3) && (name == "Joe")) { Console.WriteLine("x is 3 and the name is Joe"); } Console.WriteLine("this line runs no matter what"); } private static void TryAnIfElse() { intx=5; if(x==10) { C ons ole. Wri teLi ne( "x must be 10 "); } el se { Console.WriteLine("x isn’t 10"); } } private static void TrySomeLoops() { int count = 0; while (count < 10) { count = count + 1; } for(inti=0;i<5;i++) { count = count - 1; } Console.WriteLine("The answer is " + count); } Что выведет на консоль метод TrySomeLoops? Что выведет на консоль метод TryAnIfElse? Что выведет на консоль метод TryAnIf? Ответы для этого упражнения в книге не приводятся. Чтобы про- верить свои ответы, просто запустите про- грамму. Возьми в руку карандаш
дальше 4 109 близкое знакомство с C# Тогда ваш цикл будет работать бесконечно. Каждыйраз,когда ваша программа проверяетусловие,результат условияможетбыть равен true или false. Если онравен true, топрограмма выполниттело цикла ещеодинраз. Послетого как код цикла будет выполнен достаточное количество раз, условие в какой-томоментдолжновернутьfalse.Впротивномслучае цикл будетпродолжатьсябесконечно,пока вы непрерветепрограмму или не перезагрузите компьютер! Авы можетепредположить, для чего может быть нужен цикл, к оторый никогда не останавливается? Мозговой штурм Это называется бесконечным ци- клом. И разуме- ется, возможны ситуации, когда вы действительно хотите использо- ва ть такой ц икл в своем коде. Несколько полезных советов по поводу кода C# ‘ Не забывайте, что все команды должны завершаться символом ; (точка с запятой). name = "Joe"; ‘ Чтобы добавить комментарий в код, начните строку с двух символов //. // Этот текст игнорируется ‘ Используйте /* и */ для обозначения начала и конца комментариев, которые могут включать разрывы строк. /* Этот комментарий * занимает несколько строк */ ‘ Объявление переменной начинается с типа, за которым следует имя. int weight; // Переменная с типом int и именем weight ‘ Во многих случаях лишние пробелы игнорируются. Такимобразом, команда int j = 1234 ; в точности эквивалентна int j = 1234; ‘ Вся суть команд if/else, while, do и for заключается в их условиях. Каждый цикл, встречавшийся нам до сих пор, выполнялся до тех пор, пока его условие оставалось истинным. Что-то здесь не так! А что произойдет с циклом, если его условие никогда не становится ложным?
110 глава 2 механика при проектировании пользовательского интерфейса Наверняка концепция механики может помочь мне в проектах любого типа, не только в играх. Безусловно! В каждой программе используются свои механики. Механикисуществуют навсехуровняхпроектированияпрограммныхпродуктов. Ихпроще обсуждать ипонять в контексте видеоигр.Мы воспользуемсяэтим об- стоятельствомдляболее глубокогопониманиямеханик, чтоможет быть полезно дляпроектирования ипостроениялюбых видов проектов. Пример:механикиигрыопределяют, насколькосложно илипросто играть внее. ЗаставьтегерояPac-Man перемещатьсябыстрее или замедлитепризраков —и игра становится проще. Она не обязательно становится лучше или хуже; она просто становитсядругой.И знаете что?Этаидея применима ик проектированиюваших классов!Вы можете задуматься надтем, как проектировать методы и поля как механики классов. Решения, которые вы принимаете относительно разбиения кодана методыили использованияполей,упрощают или усложняютработус ними. М ехани ка К игровой механике относятся аспекты игры, составляющие реальный игровой процесс: правила, возможные дей- ствия игрока и поведение игры в ответ на эти действия. • Начнем с классической видеоигры. К механикам игры Pac-Man относится управление игровым персонажем с джойстика, количество очков за точки и «энергетические таблетки», поведение призраков, продолжительность их перехода в синее (пассивное) состояние, изменение их поведения после того, как игрок съест энергетиче- скую таблетку, условие получения дополнительных жизней игроком, замедление призраков при движении по туннелю — все правила, определяющие игру. • Говоря о механике, разработчики игры часто имеют в виду отдельный режим взаимодействия или управления: например, двойной прыжок в платформенной игре или защита, способная получить определенное количество попаданий в шутере. Часто бывает полезно выделить отдельную механику для проверки и усовершенствования. • Настольные игры предоставляют отличные возможности для понимания концепций механики. Генераторы случайных чисел (кубики, карты и т. д .) являются отличными примерами конкретных механик. • Вам уже встречался хороший пример механики: таймер, добавленный в игру с поиском пар, полностью изменя - ет впечатление от игры. Таймеры, препятствия, враги, карты, гонки , призовые очки... все это механики . • Разные механики комбинируютсяразличными способами, и это может оказать большое влияние на опыт игрока. «Монополия» — хороший пример игры, объединяющей два разных генератора случайных чисел (карты и куби- ки), чтобы сделать игровой процесс более интересным и нетривиальным. • К игровым механикам также относятся особенности структурирования данных и организации кода, рабо- тающего с данными, даже если эти механики не были введены намеренно! Вспомните легендарную ошибку 256-го уровня Pac-Man. На этом уровне из-за ошибки в коде экран наполовину заполнялся «мусором», а игра становилась невозможной. Эта ошибка стала частью игровых механик. • Таким образом, когда мы говорим о механиках игры C#, в эту категорию также включаются классы и код, по - тому что они управляют работой игры. Разработка игр... и не только
дал ьше 4 111 близкое знакомство с C# Элементы управления определяют механики ваших пользовательских интерфейсов В предыдущей главе для построения игры использовались элементы управленияTextBlock иGrid. Однако существует многоразных способов использования элементов,а решения, принятые вами при выбореэлементов,могут очень сильноизменить вашеприложение.Звучит неожиданно?На самом деле все этооченьпохоже на принятиерешенийприпроектированииигр.Есливысоздаете настольную игру, которойнужен генератор случайныхчисел, выможетевоспользоватьсякубикамииликартами. Есливы работаете надигрой-платформером,вы можетерешить,что игровойперсонаж долженуметь прыгать, делать двойнойпрыжок,делать прыжок отстены илилетать(иливыполнятьразные действиявразное время).Тожеотноситсякприложениям:есливыразрабатываетеприложение,вкоторомпользователь должен ввестичисло, выможетевыбрать дляэтойцелиразные элементы— иотвашеговыборазависят впечатления пользователя приработес приложением. Элементы управления — компоненты пользовательского интерфейса (UI), строительные блоки для построения UI.От выбора элементов управления зависит механика вашего приложения. Позаимствуем идею механики из ви- деоигр. Она поможет лучше понять возможные варианты и принять правильные решения в любых прило- жениях (не только видеоиграх). Ì В текстовомполепользователь может ввестилюбой текстпо своему усмотрению.Приэтом вданном примере необходимо проверить, что пользователь вводит только числа, анепроиз- вольныйтекст. Ì Список предоставляет пользо- вателювозможностьвыбрать элементиз нескольких вариан- тов. Длинные спискиобычно снабжаютсяполосой прокрут- ки, чтобы пользователюбыло проще найтинужноезначение. Ì Переключателиограничи- вают выбор пользователя несколькимификсирован- ными вариантами (напри- мер, их можноиспользо- ватьдлявыбора чисел). Вы можете разместить их так, как считаетенужным. Ì Другие элементыуправления наэтой странице могутиспользоваться для других типов данных, но ползунки предназначены исключительно для выборачисел.Впринципе, телефонные номератожеявляютсяобычными числами, так что формально ползунок может использоваться для выбора телефона. Как вы думаете, хорошая лиэтомысль? Ì Поле со списком объединяет поведение спискаи тексто- вогополя. Оновыглядит как обычное текстовое поле, но когдапользовательщелкает на списке,под ним открывается сп исо к. В редактируемом поле со списком пользо- ватель может либо выбрать значение из списка, либо ввести собственное значение. В оставшейся части этой главы рассматривается проект для построения настольного приложения WPF для Windows. За соответствующим проектом для macOS обращайтесь к приложению «Visual Studio для пользователей Mac».
112 глава 2 так много способов ввода чисел Создание приложения WPF для экспериментов с элементами управления Есливы заполнялиформу на веб-странице, то вы уже видели все элементы управления спредыдущейстраницы (даже есливыне знали ихофициальные названия).Теперь создадим проектWPF,чтобы немного потренироваться в использовании этих элементов. Приложениебудет очень простым — оно предлагает пользователю ввестичисло,послечего выводитвыбранноечисло. НестарайтесьзапомнитьразметкуXAMLдля этих элементов управления. Предназначение врезки Сделайте это! иупражне- ний — немного потренироваться в использованииXAML для построенияUI с элементами. Вы всегда можете вернуться кним, когда мы будем использовать эти элементы позднее в книге. Элемент TextBlock — такой же, как в игре с поиском пар. Каждый раз, когда вы ис- пользуете любой другой элемент для выбора числа, этот элемент TextBlock будет обновляться выбранным числом. Элемент ListBox позволяет вы- брать число из списка. Элемент ComboBox тоже позволяет выбрать чис- ло из списка, но список открывается только по ще лч ку. Элемент TextBox пред- назначен для ввода текста. Мы добавим код, чтобы в те кст овом поле можно было вводить только число- вые данные. Шесть разных переключателей (элементы RadioButton). При установке любого переключателя элемент TextBlock обновляется соответствующим числом. Еще один элемент ComboBox. Он отличается от предыду- щего, потому что является редактируемым. Это означает, что пользователь может либо выбрать число из списка, либо ввести его самостоятельно. Эти два ползунка предназначены для выбора чисел. Верхний пол- зунок позволяет выбрать число от 1 до 5. Нижний ползунок позволяет выбрать телефонный номер — просто чтобы дока- зать, что это возможно. РЕ Л А К С Сделайте это!
дальше 4 113 близкое знакомство с C# В главе 1 мы добавляли определения строк и столбцов сетки в приложение WPF, а именно была создана сетка с пятью строками равной высоты и четырьмя столбцами равной ширины. То же самое будет сделано и в этом приложении. Создайте новый проект WPF Запустите Visual Studio 2019 и создайте новый проект WPF, как было сделано в игре с поиском пар в главе 1. Выберите команду Create a new project и выберите вариант WPFApp (.NET Core). Присвойте проекту имя ExperimentWithControls. Выберите текст заголовка окна Измените свойство Title тега <Window> и задайте текст заголовка окна Experiment With Controls. Добавьте строки и столбцы Добавьте три строки и два столбца. Высота каждой из первых двух строк должна быть вдвое больше высоты третьей, а два столбца должны иметь одинаковую ширину. А вот как окно должно выглядеть в конструкторе: Окно содержит два столбца одинаковой ширины. Окно содержит три строки. Высота первых двух строк вдвое больше высоты последней строки. Упражнение
114 глава 2 начинаем добавлять элементы Ниже приведен код XAML главного окна. Мы использовали более светлый шрифт для кода XAML, который был сгенерирован Visual Studio за вас и который вам не пришлось изме- нять. Свойство Title в теге <Window> было изменено, после чего были добавленыразделы <Grid.RowDefinitions> и <Grid.ColumnDefinitions>. < Win dow x:C las s=" Expe rim entW ith Con trol s.M ain Wind ow" x mln s="h ttp ://s che mas .mic ros oft .com /wi nfx/ 200 6/x aml/ pre sen tati on" x mln s:x= "ht tp:/ /sc hem as.m icr oso ft.c om/ winf x/2 006 /xam l" x mln s:d= "ht tp:/ /sc hem as.m icr oso ft.c om/ expr ess ion /ble nd/ 200 8" x mln s:mc ="h ttp: //s che mas. ope nxml for mat s.or g/m ark up-c omp ati bili ty/ 200 6" x mlns :lo cal ="cl r-n ame spac e:E xper ime ntW ithC ont rol s" m c:Ig nor abl e="d " Title="Experiment With Controls" Height="450" Width= "800"> <Grid> < Grid .Ro wDe fini tio ns> < Row Def init ion /> < Row Def init ion /> < Row Def init ion Hei ght =". 5*"/ > < /Gri d.R owD efin iti ons > < Grid .Co lum nDef ini tio ns> < Col umn Defi nit ion /> < Col umn Defi nit ion /> < /Gri d.C olu mnDe fin iti ons> </ Gri d> </Window> Уверен, сейчас самое время добавить проект в систему управления версиями... «Сохраняйтесь пораньше, сохраняйтесь почаще». Этастарая поговорка существовала еще до того, как ввидеоиграхпоявиласьфункцияавтосохранения, когда для создания резервной копии проекта в компьютер приходилось вставлять одну из таких штук... Но это отличный совет! Visual Studio упрощает добавление проектов в систему управления версиями и поддержа- ниеихв актуальном состоянии, чтобы вы всегда могли вернутьсякстарой версии и просмотретьвсеизменения, внесенные с техпор. В результате назначения нижней строке высоты .5* ее высота будет равна половине высоты каждой из двух других строк. Также можно было за- дать двум другим строкам высоту 2* (или задать двум верхним строкам высоту 4*, а нижней высоту 2*, или задать двум верхним строкам высоту 1000*, а нижней строке 500*, и т. д .) . Измените свойство Title окна, чтобы задать текст заголовка. Упражнение Решение
дальше 4 115 близкое знакомство с C# В главе 1 мы добавили элементы TextBlock во многие ячейки сетки и поместили в каждый из них символ ?. Также элементу Grid и каждому из эле- ментов TextBlock будет присвоено имя. Для этого проекта добавьте один элемент TextBlock, при- свойте ему имя number, назначьте текст # с размером шрифта 24px и выровняйте его по центру правой верхней ячейки сетки. Упражнение M I N I Добавление элемента TextBox в приложение ЭлементTextBox предоставляет пользователю поле для ввода текста; добавим его вваше приложение.Но намне хотелосьбыиметьTextBoxбезпояснительной надписи, поэтому сначала будет добавлен элементLabel(который во многом похож на TextBlock, не считая того, что он предназначен специально для до- бавления пояснений к другим элементам). Перетащите элемент Label с панели инструментов в левую верхнюю ячейку сетки. Именно так добавлялись элементы TextBlock в игре с поиском пар из главы 1, но только на этот раз это делается с элементом Label.Неважно, в какую именно позицию ячейки вы ее перетащите, главное, чтобы это была левая верхняя ячейка. Задайте размер текста и содержимое элемента Label. Пока элемент Label остается выделенным, перейдите в окно Properties, раскройтераздел Textи выберитеразмер шрифта 18px. Затемраскройте раздел Common и задайте свойству Content текст Enter a number. Перетащите элемент Label в левый верхний угол ячейки. ЩелкнитенаэлементеLabel вконструкторе иперетащите его в левый верхнийугол. Когда он будетнаходиться в пределах 10пикселовотлевогоиливерхнегокраяячейки, серые полосы исчезнут и элементбудет привязан к отступуразмером10px. В коде XAML должен появиться элемент Label: <Label Content="Enter a number" FontSize="18" Margin="10,10,0 ,0" HorizontalAlignment="Left" VerticalAlignment="Top"/> 1 2 3
116 глава 2 код программной части и xaml Перетащите элемент TextBox в левую верхнюю ячейку сетки. В вашем приложении элемент TextBox будет располагаться под Label, чтобыпользователь могвводить числа.Перетащитеего так, чтобыонрас- полагалсяу левого краяячейкиподLabel, — появятся те же серыеполосы для размещения его непосредственно под Label с левым отступом10px. Назначьтеэлементу имя numberTextBox, размер шрифта 18px и текст 0. 4 Ниже приведен код XAMLэлемента TextBlock, располагающегося в правой верхней ячейке сетки. Вы можете воспользоваться визуальным конструктором или ввести XAML вручную. Главное — проследите за тем, чтобы свойства вашего элементаTextBlock точно соответствовали свойствам в приведенном решении, но, как и прежде, у вас свойства могут следовать в другом порядке. <TextBlock x:Name="number" Grid.Column="1" Text="#" FontSize="24" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"/> Когда вы используете серые полосы для размещения элемента управ- ления, он фиксируется в позиции с отступом 10px под элементом, расположенным выше. Вы увидите, как левые и верхние отступы изме- няются при перетаскивании. <Label Content="Enter a number" FontSize="18" Margin="10 ,10 ,0,0 " Hori zon tal Alig nme nt= "Lef t" Vert ica lAl ignm ent ="To p" /> <T ext Box x:N ame ="nu mbe rTex tBo x" Font Siz e="1 8" Mar gin= "10 ,49, 0,0 " T ext= "0" Wi dth= "12 0" Ho riz ont alAl ign men t="L eft " Te xtW rap ping ="W rap" Ve rti calA lig nme nt=" Top " /> <TextBlock x:Name="number" Grid.Column="1" Text="#" FontSize="24" H ori zon talA lig nme nt=" Cen ter" Ve rti calA lig nmen t=" Cen ter" Te xtW rapp ing ="Wr ap" /> Ваше окно сейчас должно выглядеть так: АкодXAML,появляющийсявнутри<Grid> за определениямистрокиистолбца,но до </Grid>, должен выглядеть так: Помните: свойства могут следовать в другом поряд- ке или быть разбитыми на строки. Упражнение Решение MI N I
дальше 4 117 близкое знакомство с C# В окне Autos выводятся переменные, используемые в команде, которая выдала исключение: number и num- berTextBox. Переменная numberTextBox содержит {System.Windows.Controls.TextBox:0}, и именно так нормаль- ный элементTextBox должен выглядеть в отладчике. Но значение number — элемента TextBlock, в которыйдолжен копироваться текст, — равно null.Позднее выузнаете, что означает null. Иэтоисключительноважная подсказка:IDE сообщает вам, что элемент TextBlock number не инициализирован. Проблема в том, что код XAML для TextBox содержит команду Text="0 " , поэтому при запуске приложение инициа- лизируетTextBox и пытается задать текст. Приэтомсрабатывает обработчик события TextChanged, который пытает- ся скопировать текст в TextBlock.Но ссылка наTextBlock все ещеравна null,поэтому приложение выдает исключение. Итак, чтобы исправить ошибку, необходимо позаботиться о том, чтобы элемент TextBlock инициализировался до TextBox. Призапуске приложения WPF элементы инициализируются в порядке их следования вXAML.Следова- тельно, ошибку можно исправить простым изменением порядка элементов в XAML. Поменяйте местами элементы TextBlock иTextBox, чтобы элемент TextBlock предшествовалTextBox: <Label Content="Enter a number" ... /> <TextBlock x:Name="number" Grid.Column="1" ... /> <TextBox x:Name="numberTextBox" ... /> Приложениедолжно выглядеть в конструкторе точно так же, как прежде, и это логично, потому что оно содержит те же элементы управления. Теперь снова запустите свое приложение. На этотраз оно запустится, и элемент TextBox будетпринимать только числовой ввод. По следу Теперь запустите приложение. Стоп!Что-то пошло не так — программа выдает иск лю ч ение. Взгляните в нижнюю часть IDE. В ней нахо- дится окно Autos, в котором выводятся все определенныепеременные. Чтожепроисходити, что ещеважнее, как с этим справиться? Для элемента TextBlock number вы- водится "null" — и это же слово присутствует в типе исключения NullReferenceException. Выделите тег TextBlock в ре- дакторе XAML и переместите его над TextBox, чтобы он ини- циализировался первым. Хороший разра- ботчик занима- ется не только написанием кода! Вы получили другое исключение, кото - рое вам придется расследовать по- добно тому, как это было сделано в главе 1. Выясне- ние причин и ис- правление подобных проблем также является очень важным навыком программиста. В резуль- тат е перемеще- ния тега TextBlock в XAML так, чтобы он рас- полагался над TextBox, элемент TextBlock будет инициали- зироваться первым.
118 глава 2 ввод числовыхданных Добавление кода C# для обновления TextBlock Вглаве1 мы добавилиобработчики событий(методы,которыевызыва- ютсяпри возникновении определенногособытия) дляобработки щелчков мышью вигре споискомпар.Теперьмыдобавимобработчиксобытиявкод программнойчасти,который вызываетсякаждыйразпривводе текста в TextBox и копирует этот текст вэлемент TextBlock, добавленный в правую верхнюю ячейку внашем мини-упражнении. Сделайте двойной щелчок на элементе TextBlock для добавления метода. Как только вы сделаете двойной щелчок на TextBox, IDE авто­ матически добавляет метод­обработчик событияC#, связан- ный с событием TextChanged этого элемента. IDE генерирует пустойметодиприсваивает емуимя,состоящееизимениэлемента (numberTextBox),символа подчеркиванияи имени обрабатываемого с об ытия — nu m be rText Bo x_Text Chan ged: private void numberTextBox_TextChanged(object sender, TextChangedEventArgs e) { } Добавьте код в новый обработчик события TextChanged. Каждыйраз, когда пользователь вводит текст в TextBox, приложение должно скопировать его в элементTextBlock,добавленныйвправый верхнийугол сетки. Так какэлементуTextBlockбыло присвоено имя(number) иу TextBox оно тоже имеется (numberTextBox), для копирования содер- жимого элемента достаточно однойстрокикода: private void numberTextBox_TextChanged(object sender, TextChangedEventArgs e) { number.Text = numberTextBox.Text; } Запустите приложение и проверьте, как работает TextBox. Запустите свое приложение при помощи кнопки Start Debugging (или командой Start Debugging (F5)изменюDebug), какэтоделалось вигре споиском пар вглаве1.(Еслипоявитсяпанельинстру- ментоввремени выполнения, ее можно отключить,как было описано в главе1.)Введите любое число в TextBox,ионо будетскопировано. Ивсежечто-то пошло нетак — в TextBox можно ввестилюбойтекст,нетолько числа! 1 2 3 Когда вы делаете двой- ной щелчок на элементе TextBox, IDE добавляет обработчик события для событияTextChanged. Этот обработчик вызывается каждый раз, когда поль- зователь изменяет текст в поле. Двойной щелчок на других типах элементов может добавлять другие обработчики событий, а в некоторых случаях (например, с TextBlock) вообще не добавляет ни- какие обработчики. Эта строка кода задает текст элемента TextBlock так, чтобы он совпадал с текстом элемента TextBox. Она вызывается каждый раз, когда пользователь изменяет текст в TextBox. Когда вы вводите число в TextBox, обработчик собы- тия TextChange копирует его в TextBlock. Должен быть какой-то способ ограничить ввод только число- выми данными! Как вы думаете, что нужно сделать?
дальше 4 119 близкое знакомство с C# Вашновыйобработчиксобытия должен состоять из одной команды: private void numberTextBox_PreviewTextInput(object sender, TextCompositionEventArgs e) { e.Handled = !int.TryParse(e.Text, out int result); } Этотобработчик событияработаетпо следующей схеме: 1. Обработчиксобытиявызывается,когда пользователь вводиттекст вTextBox, но до обновления TextB o x. 2. Специальныйметодint.TryParse используетсядляпроверкитого, что введенныйпользователем текст является числом. 3. Если пользователь ввел число, e.Handled присваивается true; тем самым вы указываетеWPF иг- норировать ввод. Прежде чем выполнять код,вернитесь и просмотрите тегXAML дляTextBox: < Tex tBox x: Nam e="n umb erTe xtB ox" Fon tSi ze= "18" Ma rgin ="1 0,4 9,0, 0" Text ="0 " W idth ="1 20" Ho riz onta lAl ign ment ="L eft" Te xtW rapp ing ="Wr ap" Ve rtic alA lig nmen t=" Top" Te xtC hang ed= "num ber Tex tBox _Te xtC hang ed" Pr evi ewTe xtI npu t="n umb erTe xtB ox_ Prev iew Tex tInp ut" /> Теперь элемент связан с двумя обработчиками событий: событие TextChange связывается с методом numberTextBox_TextChanged, а прямо под ним событиеPreviewTextInput связывается с методом numberTextBox_PreviewTextInput. Добавление обработчика события, который разрешает вводить только числовые данные Чтобы добавить обработчик событияMouseDown к элементу TextBlock вглаве 1,мывоспользовались кнопкойвправомверхнемуглу окна свойств дляпереключениямеждусвойствами исобытиями. Теперь мысделаем то же самое, только на этот разсобытиеPreviewTextInput будет использоваться дляпринятияввода, состоящего толькоизчисловыхданных, иотклонения всех остальных вводимых символов. Есливашеприложениевыполняетсявнастоящиймомент, остановитеего. Перейдите вотладчик, щелкните наэлементе TextBox, чтобывыделить его, иперейдитевокносвойств, чтобыпросмотреть его события. Прокрутите списокисделайте двойной щелчокв поле рядомсPreviewTextInput, чтобы IDEсгенерировала метод-обработчик события. Кнопка с гаечным ключом в правом верхнем углу окна свойств выводит свойства выделенного эле- мента. Кнопка с молнией возвращается к выводу обработчиков событий. Сделайте двойной щелчок Выделите TextBox в конструк- торе, после чего используйте кнопку с молнией в окне свойств для просмотра событий. Вы узнаете все об int.TryParse позднее в этой книге — пока просто введите код точно в таком виде, в каком он при- веден здесь. Сделайте это!
120 глава 2 упражнение на построение пользовательского интерфейса Добавьте остальные элементыXAML для приложения ExperimentWithControls: переключатели, список, две разновидности переключателей и два ползунка. Каждый элемент обновляет содер- жимое элемента TextBlock в правой верхней ячейке сетки. Добавление переключателей в левую верхнюю ячейку рядом с TextBox Перетащите элемент RadioButton с панели инструментов в левую верхнюю ячейку сетки. Перемещайте его, пока ле- вый край не будет выровнен по центру ячейки, а верхний — по верхнему краю TextBox. Во время перетаскивания элементов в конструкторе появляются направляющие, которые помогают аккуратно выровнять элементы; положение элемента фиксируется по этим направляющим. Раскройтераздел Common окна свойств и задайте свойству Content элементаRadioButton значение1. Затем добавьте еще пять элементов RadioButton, выровняйте их и задайте их свойства Content.Но на этот раз не пе- ретаскивайте их с панели инструментов. Вместо этого щелкните на RadioButton на панели инструментов, а затем щелкните внутри ячейки. (Дело в том, что если вы перетаскиваете элемент с панели инструментов в тот мо- мент, когда выделен элемент RadioButton, IDE вложит новый элемент в RadioButton. Вложение элементов рас- сматривается позднее в книге.) Добавление списка в левую среднюю ячейку сетки Щелкнитенаэлементе ListBox панели инструментов, затем щелкните внутри левой средней ячейки для добавления элемента. В разделе Layout задайте всем отступам размер 10. Вокнесвойств отображаются обработчики событий, анесвойства? Воспользуйтесь кнопкой длявывода свойств;если вы использовали полепоиска, незабудьте очистить его содержимое. Вертикальная направля- ющая появляется в тот момент, когда левый край перетаскиваемого элемен- та выравнивается по центру ячейки. Горизонтальные направляющие по- являются при выравнивании элемен- та по верхнему краю, середине или нижнему краю другого элемента. При добавлении каждого переклю- чателя можно ис- пользовать полосы и направляющие для выравнивания его по другим эле- ментам. Когда вы добавляете ListBox в ячейку и задаете отсту- пы 10, элемент выглядит как пустой прямоугольник в левой средней ячейке. Упражнение
дальше 4 121 близкое знакомство с C# Присвойте элементу ListBox имя myListBox и добавьте в него элементы ListBoxItem Элемент ListBox предназначен для выбора вариантов. Для этого в список необходимо добавить варианты. Выберите элемент ListBox, раскройте раздел Common в окне свойств и щелкните на кнопке Edit Items рядом с пунктом Items ( ). Добавьте пять элементов ListBoxItem и при- свойте их значениям Content числа от 1 до 5. Элемент ListBox должен выглядеть так: Добавьте два элемента ComboBox в левую среднюю ячейку сетки Щелкните накнопкеэлементе ComboBox на панели инструментов, затем щелкните в правой средней ячейке, чтобы добавить элемент ComboBox, и присвойте ему имя readOnlyComboBox. Перетащите элемент в левый верхний угол и при помощи серых полос назначьте ему левый и верхний отступ 10. Затем добавьте еще один элемент ComboBox с именем editableComboBox в ту же ячейку и выровняйте его по правому верхнемууглу. Воспользуйтесь окном Collection Editor для добавления тех же элементовListBoxItem с числами 1, 2, 3 ,4 и 5 в оба элемента ComboBox — сначала это следует проделать с первым элементом ComboBox, затем со вторым. Наконец,разрешитередактированиедля элемента ComboBox справа — раскройтеразделCommon в окне свойств и установите флажок IsEditable. Теперь пользователь может ввести свое число в элементе ComboBox. Добавьте пять эле- ментов ListBoxItem. В разделе Common задайте значению Content каждого из них число (1, 2, 3, 4 или 5). Щелкните на кнопке Edit Items, чтобы открыть окно Collection Editor. Добавьте каждый элемент списка: выберите в раскрывающемся списке ListBoxItem и щелкните на кнопке Add. Редактируемый элемент ComboBox отличается от нередактируемого, чтобы пользователь знал, что он может либо ввести собственное значение, либовы- брать значение из списка. Упражнение
122 глава 2 у ползунков есть свое применение(и ограничения) Ниже приведен код XAML для элементов RadioButton, ListBox и двух элементов ComboBox, добав- ленныхв упражнении. Код XAMLдолженнаходиться в самом низу содержимого сетки — вы найдете эти строки рядом с закрывающим тегом </Grid>. Как и в другом коде XAML, который встречался вам ранее, в вашем коде свойства в теге могут следовать в другом порядке или разрывы строк могут находиться в другихместах. <RadioButton Content="1" Margin="200,49,0 ,0" Hor izo ntal Ali gnm ent= "Le ft" Ver tic alAl ign men t="T op" /> <RadioButton Content="2" Margin="230,49,0 ,0" Hor izo ntal Ali gnm ent= "Le ft" Ver tic alAl ign men t="T op" /> <RadioButton Content="3" Margin="265,49,0 ,0" Hor izo ntal Ali gnm ent= "Le ft" Ver tic alAl ign men t="T op" /> <RadioButton Content="4" Margin="200,69,0 ,0" Hor izo ntal Ali gnm ent= "Le ft" Ver tic alAl ign men t="T op" /> <RadioButton Content="5" Margin="230,69,0 ,0" Hor izo ntal Ali gnm ent= "Le ft" Ver tic alAl ign men t="T op" /> <RadioButton Content="6" Margin="265,69,0 ,0" Hor izo ntal Ali gnm ent= "Le ft" Ver tic alAl ign men t="T op" /> <ListBox x:Name="myListBox" Grid.Row="1" Margin="10 ,10,10,10"> <L ist BoxI tem Co nten t=" 1"/ > <L ist BoxI tem Co nten t=" 2"/ > <L ist BoxI tem Co nten t=" 3"/ > <L ist BoxI tem Co nten t=" 4"/ > <L ist BoxI tem Co nten t=" 5"/ > < /Li stB ox> <ComboBox x:Name="readOnlyComboBox" Grid.Column="1" Margin="10,10 ,0,0" Grid.Row="1 " Ho rizo nta lAli gnm ent ="Le ft" Ver tic alA lign men t="T op" Wi dth= "12 0"> <L ist BoxI tem Co nten t=" 1"/> <L ist BoxI tem Co nten t=" 2"/> <L ist BoxI tem Co nten t=" 3"/> <L ist BoxI tem Co nten t=" 4"/> <L ist BoxI tem Co nten t=" 5"/> < /Co mbo Box> <ComboBox x:Name="editableComboBox" Grid.Column="1" Grid.Row="1" IsEditable="True" H ori zon talA lig nme nt=" Lef t" V ert ical Ali gnme nt= "To p" W idth ="1 20" Mar gin ="27 0,1 0,0, 0"> <L ist BoxI tem Co nten t=" 1"/> <L ist BoxI tem Co nten t=" 2"/> <L ist BoxI tem Co nten t=" 3"/> <L ist BoxI tem Co nten t=" 4"/> <L ist BoxI tem Co nten t=" 5"/> < /Co mbo Box> IDE добавляет свойства, опре- деляющие от- ступы и вы- равнивание, при перетаскивании каждого элемен- та RadioButton. Когда вы запускаете свою программу, она должна выглядеть примерно так. Вы можете использовать все элементы управления, но только TextBox обновляет значение наверху справа. Два элемента ComboBox отличаются только свойством IsEditable. Когда вы используете окно Collection Editor для до- бавления элементов ListBoxItem в элемент ListBox или ComboBox, оно создает закрывающий тег </ListBox> или </ComboBox> и добавляет теги <ListBoxItem> между открывающим и закрывающим тегами. Убедитесь в том, что элементу ListBox и двум элементам ComboBox присвоены правильные имена. Они будут ис- пользоваться в коде C#. Упражнение Решение
дальше 4 123 близкое знакомство с C# Добавление ползунков в нижнюю строку сетки Добавимдваползункав нижнююстроку,а затем назначим им обработчики событий, чтобыониобновлялиэлементTextBlockвправом верхнемуглу. Добавьте ползунок в приложение. Перетащите элемент Slider с панели инструментоввправую нижнюю ячейку. Разместитееговлевомверхнемуглуячейкии воспользуйтесь серымиполосами, чтобыустановить для него левый иверхний отступразмером10. Используйтераздел Common окна свойств, чтобы присвоить AutoToolTipPlacement значение TopLeft, Maximum — значение 5 иMinimum — значение 1.При- свойте элементуимя smallSlider.Затемсделайте двойной щелчокна ползунке, чтобыдобавить этот обработчик: private void smallSlider_ValueChanged( object sender, RoutedPropertyChangedEventArgs<double> e) { number.Text = smallSlider.Value.ToString("0"); } Добавьте ползунокдля выбора телефонных чисел. Есть старая поговорка: «Даже если идея ужасная и, возможно, дурацкая, это еще не значит, что от нее нужно отказаться». Так что мы сделаем нечто такое, что выглядит довольно глупо: добавимползунокдлявыбора телефонныхномеров. Перетащите другой ползунок в нижнюю строку.Ис- пользуйтеразделLayoutокнасвойств, чтобысбросить егоширину, задайте свойствуColumnSpan значение2, задайте всем отступам значение 10, выберите верти- кальное выравнивание Center и горизонтальное вы- равниваниеStretch.ЗатемвразделеCommonзадайте свойствуAutoToolTipPlacementзначение TopLeft, свойствуMinimum — значение 111111111,свойствуMaximum—значение9999999999 исвойствуValue— значение 7183876962.Присвойте элементуимяbigSlider, затемсделайте нанемдвойной щелчоки добавьте следующийобработчиксобытияValueChanged: private void bigSlider_ValueChanged( object sender, RoutedPropertyChangedEventArgs<double> e) { number.Text = bigSlider.Value.ToString("000-000 -0000"); } 1 2 Чтобы найти элемент Slider на панели ин- струментов, необходи- мо раскрыть раздел All WPF Controls и про- крутить его почти до самого конца. Значение элемента Slider представляет собой дробное число. Этот символ «0» преобразует его в целое число. Нули и дефисы нужны для того, чтобы метод преобразовывал любое число из 10 знаков в формат телефонного номера США.
124 глава 2 завершение приложения Добавление кода C#, обеспечивающего работу элементов управления Каждый элементуправления в нашем приложении должен делать одно и то же: обновлять TextBlock в правом верхнем углу числом,так что приустановке переключателя или выборе элемента изListBox или ComboBox элемент TextBlock обновляется выбранным значением. Добавьте обработчик события Checked к первому элементу управления RadioButton. Сделайте двойной щелчок на первом элементеRadioButton. IDE добавляет новый метод-обработчик события с именем RadioButton_Checked (так как вы еще не присвоили элементуимя,длягенерированияметода используется тип элемента).Добавьтеследующую строкукода: private void RadioButton_Checked( object sender, RoutedEventArgs e) { if (sender is RadioButton radioButton) { number.Text = radioButton.Content.ToString(); } } Назначьте тот же обработчик события другим элементам RadioButton. Внимательно присмотритесь к коду XAML только что измененного элемента RadioButton. IDE добавляет свойство Checked="RadioButton_Checked" — им енно т а к к э л ементу подключались дру- гиеобработчики событий.Скопируйте свойство в другие тегиRadioButton,чтобы они имели одинаковые свойстваChecked, — в результате все они связываются с одним обработчиком события Checked.Выможетевоспользоватьсярежимом просмотра событийвокне свойств, чтобыубедиться в том,что обработчики всех элементовRadioButton были назначены правильно. Заставьте ListBox обновлять TextBlock в правой верхней ячейке. Выполняяупражнение,выприсвоилисвоему элементуListBoxимяmyListBox. Теперь добавимоб- работчиксобытия, которыйбудет срабатыватькаждыйраз, когда пользователь выбираетэлемент в списке ииспользуетимядля получениявыбранногочисла. Сделайте двойной щелчоквнутри пустого пространствавListBoxподэлементами, чтобыIDE добавила метод-обработчик для событияSelectionChanged. Добавьте следующую команду: private void myListBox_SelectionChanged( object sender, SelectionChangedEventArgs e) { if (myListBox.SelectedItem is ListBoxItem listBoxItem) { number.Text = listBoxItem.Content.ToString(); } } 1 2 3 Эта команда использует ключевое слово is, о котором вы узнаете в главе 7. А пока про- сто аккуратно введите команду точно так, как она приведена на странице (и сделайте то же самое для остальных методов-обра- ботчиков). Гот овый код Если переключить окно свойств в режим событий, вы можете выбрать любой из элементов RadioButton и убедиться в том, что у всех элементов событие Checked связано с обработчиком события RadioButton_Checked. Проследите за тем, чтобы щелчок был сделан в пустом пространстве под элемента- ми списка. Если вы щелкнете на элементе списка, то обработчик будет добавлен только для этого элемента, а не для всего элемен- та ListBox.
дальше 4 125 близкое знакомство с C# Заставьте поле со списком, доступное только для чтения, обновлять TextBlock. Сделайте двойной щелчок на элементе ComboBox, доступном только для чтения, чтобы среда Visual Studio добавила обработчик для события SelectionChanged, которое выдается при выборе нового значенияв ComboBox. Ниже приведенкод — оночень похож на кодListBox: private void readOnlyComboBox_SelectionChanged( object sender, SelectionChangedEventArgs e) { if (readOnlyComboBox.SelectedItem is ListBoxItem listBoxItem) number.Text = listBoxItem.Content.ToString(); } Заставьте редактируемое поле со списком обновлять TextBlock. Редактируемое поле со списком напоминает гибрид ComboBox и TextBox. Вы можете выбирать значения из списка, но также можете ввести собственный текст. Так как элементработает как TextBox, стоит добавить обработчик события PreviewTextInput и ограничить ввод числами, как этобыло сделанодля TextBox. Собственно,можно даже повторно использоватьобработчик, уже добавленный для TextBox. Перейдите ккодуXAMLредактируемогоэлементаComboBox, установите курсор непосредственно перед закрывающей угловой скобкой > и начните вводить имя PreviewTextInput. На экране появ- ляется окно IntelliSense,которое помогаетзавершить имя события. Затем добавьте знак = — как только выэтосделаете, IDEпредложитдобавить новыйобработчикиливыбратьужедобавленный. Выберите существующий обработчик события. Предыдущие обработчики событий использовали элементы списка для обновленияTextBlock. Однако в ComboBox пользователь может ввестипроизвольныйтекст,поэтому на этотраз мы до- бавим другой обработчик события. СноваотредактируйтекодXAMLидобавьте новый тегпод ComboBox.НаэтотразвведитеTextBoxBase.; кактолько вывведете точку, автозамена предложит возможные варианты. Выберите TextBoxBase. TextChanged ивведите знак =. Выберите враскрывающемся списке <NewEvent Handler>. IDEдобавляетновый обработчиксобытияв кодпрограммнойчасти. Онвыглядит так: private void editableComboBox_TextChanged(object sender, TextChangedEventArgs e) { if (sender is ComboBox comboBox) number.Text = comboBox.Text; } Теперь запустите своюпрограмму.Всеэлементыдолжныработать. Превосходно! 4 5 Вы также можете вос- пользоваться окном свойств для добавления события SelectionChanged. Если вы случайно сделае- те это, нажмите кнопку отмены (но проследите за тем, чтобы это было сделано в обоих файлах).
126 глава 2 выбор наиболее подходящего элемента ¢ Приложение состоит из классов, классы содержат ме- тоды, а методы состоят из команд. ¢ Каждый класс принадлежит некоторому простран- ству имен. Некоторые пространства имен (например, System.Collections.Generic) содержат классы .NET. ¢ Классы могут содержать поля, которые существуют за пределами методов. Разные методы могут работать с одним полем. ¢ Если метод помечается ключевым словом public, это означает, что он может вызываться из других классов. ¢ Консольные приложения .NET Core представляют собой кроссплатформенные программы, не имеющие графического интерфейса. ¢ IDE строит ваш код, чтобы преобразовать его в дво- ичныйфайл, т . е . файл, который можно запуститьдля выполнения. ¢ Кроссплатформенное консольное приложение .NET Core можно преобразовать программой командной строки dotnet, чтобы построитьдвоичные файлы для разных операционных систем. ¢ Метод Console.WriteLine выводит строку на консоль. ¢ Переменные должныбыть объявлены, прежде чем их можно будет использовать в программе. При объявле- нии переменной можно присвоить исходное значение. ¢ Отладчик Visual Studio позволяет приостановить приложение и проанализировать значения перемен- ны х. ¢ Элементы управления выдают события для многих изменений: щелчков мыши, изменений выделения, ввода текста и т. д . Иногда говорят, что события ини- циируются или генерируются, — эт о т о ж е сам о е. ¢ Обработчики событий — методы, которые вызыва- ются при выдаче события для реакции на это событие (обработки). ¢ Элементы TextBox могут использовать событие PreviewTextInput для подтверждения или отклонения введенного текста. ¢ Ползунки отлично подходят для ввода числовых дан- ных, но для выбора телефонных номеров их лучше не использовать. КЛЮЧЕВЫЕ МОМЕНТЫ Столько разных способов выбора чисел! У меня появляется НЕВЕРОЯТНО много вариантов при разработке приложения. Элементы управления дают вам гибкость для того, чтобы упростить жизнь пользователей. КогдавыстроитеUIдляприложения, вамприходитсяпринимать много разных решений: какие элементы использовать, где разместить каж- дыйэлемент,что делать с входнымиданными. Выбор того илииного элемента дает неявный сигнал относительно того, как пользоваться вашим приложением. Например, при виде набора переключателей вы знаете, что нужно выбрать одно значение из небольшого набора, тогда как редактируемое поле со списком говорит о том, что выбор практическинеограничен. Недумайте, что задачей проектирования UI является поиск «правильных» решений вместо «неправильных». Вместо этого считайте, что вы должны упростить жизнь вашим поль- зователям,насколько это возможно.
Лабораторный курс Unity No 1 https://github.com/head-first-csharp/fourth-edition Head First C# Лабораторный курсUnityNo 1 127 Добро пожаловать на первый урок «Лабораторный курс Unity». Написание кода — навык, и, как и любой другой навык, он развивается за счет практики и экспери- ментирования. И в этом отношении Unity может стать очень полезным инструментом. Unity — кроссплатформенный инструмент разработки игр, который может использоваться для создания игр профес- сионального уровня, симуляторов и многих других прило- жений. Также Unity предоставляет интересные и приятные возможности потренироваться в применении средств и идей C#, представленных в книге. Мы разработали эти короткие, целенаправленные лабораторные работы для за- крепления только что описанных концепций и приемов, которые помогут вам отточить навыки C#. Лабораторные работы необязательны, но они станут по- лезной практикой, даже если вы не планируете исполь- зовать C# для построения игр. В первой лабораторной работе мы введем вас в курс дела. Вы начнете ориентироваться в редакторе Unity, а также создавать 3D-объекты и оперировать ими. Лабораторный курс Unity No 1 Исследование C# с Unity
128 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 1 Unity — мощный инструмент для разработки игр ДобропожаловатьвмирUnity — полнофункциональнойсистемыдлясозданияигр профессионального уровня(какдвумерных(2D),таки трехмерных(3D)),а такжемоделирования,разработки ипроектиро- вания. Unity включает ряд мощных функций, в том числе... В лабораторной работе Unity мы будем рассматривать Unity как средство для изучения C#, а также для практического применения инструментов и идей C#, о которых вы узнали в книге. Учебныйстиль «ЛабораторныхработUnity» полностью ориентирован наразработчика. Ихцель — по- мочь вам как можно быстрее войти в курс дела. Приэтоммыуделяем первоочередное внимание лег- кости изложения,котороеприменяетсяв книге,чтобы вы прошли целенаправленную иэффективную тренировку вприменении концепцийиприемовC#. Крос спл ат ф орме нный и гровой дви жо к Игровойдвижокобеспечивает отображениеграфики, управление2D-или 3D-персонажами, обнаружениестолкновений, моделирование поведения реальных физических объектов и многое, многое другое. Unityреализует всеэтивозможностидля 3D-игр,которыебудут построеныв книге. Экосистема для создания игр Кроменевероятно мощных средств для создания игр,Unity также представляет экосистему, которая поможет вам в изучении и по- строении приложений.На странице Learn Unity(https://unity.com/ learn) приведеныполезные учебныересурсыдлясамостоятельного изучения, а форумы Unity (https://forum.unity.com) позволят свя- заться с другимиразработчиками игр и задать им вопросы. Unity AssetStore (https://assetstore.unity.com) предоставляет какплатные, таки бесплатныересурсы(персонажи,фигурыиэффекты), которые вы можетеиспользовать в своих проектахUnity. Мощный редактор сцен Вы проведете немало времени вредактореUnity.Он позволяет редактироватьуровни,наполненные2D- и3D-фигурами,атакже предоставляет средства для построения полноценных игровых мировввашихиграх. ИгрыUnity используютC#дляопределения своегоповедения, а редакторUnityинтегрируетсясVisualStudio, чтобы предоставить в ваше распоряжение эффективную среду разработкиигр. Хотя «Лабораторные работы Unity» направлены на разработку C# в Unity, если вы являетесь дизайнером или специалистом по компьютерной графике, редактор Unity содержит много средств, предназначенных для вас. С ними можно ознакомиться по адресу https://unity3d. com/unity/features/editor/art-and-design.
Лабораторный курс Unity No 1 Head First C# Лабораторный курсUnityNo 1 129 Uni ty Hub може т вы гля де т ь немн ого по-дру гому. Снимки экрана, приведенные в книге, были сделаны в Unity 2020.1 (Personal Edition) и Unity Hub 2.3.2. При помощи Unity Hub можно установить несколько разных версий Unity на одном компьютере, но установить можно только новейшую версию Unity Hub. Группа разработки Unity постоянно улуч- шает UnityHub и редактор Unity, и может оказаться, что увиденное вами будет отличаться от иллюстраций на этой странице. Мыбудем обновлять «Лабораторныеработы Unity» для новых изданий «HeadFirst C#». PDF-файлы обновленных лабораторных работ будут доступны на нашей странице GitHub: https://github.com/ head-first-csharp/fourth-edition. Загрузка Unity Hub UnityHub —приложениедляуправленияпроектамиUnityиустанов- ленными экземплярамиUnity, которое также становитсяотправной точкой длясозданиянового проектаUnity.Начните сзагрузкиUnity Hub по адресуhttps://store.unity.com/download, затем установите изапустите программу. Щелкните на ссылке Installs, чтобы управлятьустановлен- ными версиями Unity. Unity Hub позволяет управлять установленными версиями и проектами Unity. Мы использовали Unity 2020.1 .3f1 для создания упражнений, так что вам стоит загрузить последний официальный выпуск с номером версии, начинающимся с 2020.1 . Когда вы щелкнете на кнопке Next, Unity Hub предложит установить модули. Никакие модули вам устанавливать не нужно, но обязательно установите документацию. UnityHub позволяет управлять несколькимиверсиямиUnity на одном компьютере, поэтомувамследуетустановить туже версию, котораяиспользоваласьдляпостроения лабораторныхработ. Щелкните на ссылкеOfficialReleases иустановите последнюю версию,котораяначинаетсясUnity2020.1, — это та же версия, котораяиспользовалась для созданияснимков экрана в лабораторныхработах. После того как версиябудет установлена, проследите за тем, чтобы онабыла назначена приоритетной. Программа установкиUnity может предложить установить другую версию Visual Studio.На одномкомпьютере также допускаетсяустановканесколькихверсийVisual Studio, но еслиувасужеустановлена одна версияVisualStudio, добавлятьдругую из программыустановкиUnity ненужно. За дополнительнойинформациейобустановкеUnityHub вWindows,macOSиLinux обращайтесь по адресуhttps://docs.unity3d.com/2020.1/Documentation/Manual/ GettingStartedInstallingHub.html. Все снимки экрана в этой книге были сделаны в бесплатном из- дании Unity Personal Edition. Вы должны ввести в Unity Hub имя пользователя и пароль своей учетной записи unity.com, что- бы активировать лицензию. Unity Hub позво- ляет установить несколько версий Unity на одном компьютере. Таким образом, даже если у вас доступна новая версия Unity, вы можете вос- пользоваться Unity Hub для установки вер- сии, которую мы использовали в лабораторных работах. Будьте ос торожн ы !
130 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 1 Использование Unity Hub для создания нового проекта Щелкните на кнопке на странице Project в Unity Hub для создания нового проекта Unity. При- свойте ему имяUnityLab 1,убедитесь в том, что выбраншаблон3D, а проект создается в подходящем месте (обычно впапкеUnityProjects в домашнем каталоге). ЩелкнитенассылкеCreate Project,чтобы создать новую папку с проектомUnity. При создании нового проекта Unity генерирует множество файлов (как и при создании новых проектов вVisual Studio).Для созданиявсех файловнового про- екта Unity может потребоваться одна-две минуты. Выбор Visual Studio в качестве редактора сценариев Unity РедакторUnityтесновзаимодействует сVisualStudioIDE, чтобыупроститьредак- тирование и отладку кода ваших игр. Таким образом,первым делом мы примем мерыктому,чтобысвязатьUnityсVisualStudio. Выберитекоманду Preferences из менюEdit(илиизменюUnity наMac),чтобы открыть окноUnityPreferences. ЩелкнитенакатегорииExternal Tools на левой панели и выберитеVisualStudio в окне External ScriptEditor. Внекоторых старых версиях Unity может присутствоватьфлажокEditor Attaching — в таком случае проследите за тем, чтобы он былустановлен (это позволит вам прово- дить отладку кода Unity в IDE). Отлично!Все готово дляпостроениявашего первого проекта Unity. Visual Studio может исполь- зоваться для отладки кода Unity. Для это- го достаточно выбрать Visual Studio в каче- стве внешнего редактора сце- нариев в на- стройках Unity. Если вы не видите VisualStudio в раскрывающемся списке ExternalScript Editor, выберите Browse... и перейдите к Visual Studio. В системе Windows это обычно файл devenv.exe из папки C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\IDE\.На Mac это обычно приложение Visual Studio из папкиApplications.
Лабораторный курс Unity No 1 Head First C# Лабораторный курсUnityNo 1 131 Управление макетом Unity РедакторUnity представляет собой аналог IDE для всех частей проектаUnity, которые не являются кодом C#. Онбудетиспользоватьсядляработы сосценами,редактированиятрехмерныхобъектов,соз- данияматериалов ит. д. Как и вVisualStudio, окна и панели редактора Unity можно переупорядочить, создаваяразныеварианты макетов окна. Найдите вкладкуScene в верхнейчасти окна.Щелкните навкладкеи перетащите ее, чтобыоткрепить окно: Попробуйте закрепить его внутриилирядом сдругимипанелями,а затемперетащите его вовнутреннюю часть редактора,чтобы окно оставалось плавающим. Выберите макет Wide МывыбралимакетWide, потомучтоон хорошо подходит дляснимковэкрана вэтихлабораторныхработах. Найдите раскрывающийсясписок ивыберите команду Wide, чтобы вашредактор Unity выглядел так же, как на наших ил- люстрациях. В макете Wideредактор Unity должен выглядеть так: Представление Scene — основное интерактив- ное представление соз- даваемого вами мира. Оно используетсядля размещения 3D-фигур, камер, источников света и всех остальных объектов в игре. После того как вы измените макет при помощи раскрывающегося списка Layout справа от панели инструмен- тов, метка раскрывающегося списка может измениться в соответствии с выбранным макетом. В окне Hierarchy выводятся все объ- екты в сцене. Окно Project ис- пользуется для работы с файлами из проекта Unity. Каждый объ- ект в игре обладает свойствами, которые мож- но просматри- вать и ре- дактировать в окне Inspector. Окно Scene используется для редактирования объектов в сцене, включая освещение, камеру и фигуры. Заметили вкладку «Game» в верхней части окна? Она позволяет переключиться на окно Game, в котором можно увидеть, как будет выглядеть игра с точки зрения игрока.
132 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 1 Сцена как 3D-среда Сразу же после запуска редактора вы оказываетесь врежимередактирования сцены. Сцены можно рассматривать как уровни в играх Unity. Каждая игра Unity состоит из одной или нескольких сцен. Каждая сцена содержитотдельную 3D-средус собственным набором источников света, фигури других 3D-объектов. Когда вы создаете проект, Unity добавляет сцену с именемSampleScene и сохраняет ее в файле с именем SampleScene.unity. Добавьте в сцену сферу командой меню GameObject>>3D Object>>Sphere. ВокнеSceneпоявляетсясфера. Все,что вы видитевокне Scene,показывается с точкизрениякамеры Scene,которая «рассматривает» сценуи воспроизводит увиденное. В окне Scene отображаются все объекты сцены с точки зрения камеры. Сетка перспективы в окне помогает увидеть, на ка - кое расстояние объекты удалены от камеры сцены. Источник света, освещающий сцену. Когда вы за- пускаете свою игру, вы видите ее с точки зре- ния камеры. А это сфера, котор ую вы добавили. Так называемые прими- тивные объекты Unity. Мы будем часто использовать их в лабораторных рабо- тах Unity.
Лабораторный курс Unity No 1 Head First C# Лабораторный курсUnityNo 1 133 Игры Unity состоят из объектов GameObject Когда вы добавили сферувсвоюсцену, тем самым вы создаете новый объект GameObject. Объекты GameObject являются одной из фундаментальных кон- цепций Unity. Каждый предмет, фигура, персонаж, источник света, камера испециальныйэффектв игреUnity является объектомGameObject. Все деко- рации, персонажи и элементы окружениявигре представляются объектами GameObject. В лабораторных работах Unity мы будем строитьразные виды объектов GameObject, включая: Сферы Кубы Пл оско сти Капсулы Цилиндры Источники света К а м еры Каждый объектGameObjectсостоитиз несколькихкомпонентов,которые опре- деляют его форму,задают его позицию инаделяютего поведением. Пример: Ì Компоненты преобразований определяют позицию и угол поворота GameObject. Ì Компоненты материалов изменяют способ визуализации GameObject, т. е. способ его прорисовкиUnity, за счет изменения цвета, отражений, уровня гладкости и т. д. Ì Компоненты сценариев используют сценарииUnity для определения по- ведения GameObject. GameObject — фундаментальная разновидность объектов в Unity, а компоненты яв- ляются основны- ми структурными элементами их поведения. В окне Inspector выво- дится информация о каждом объекте GameObject в сцене и его компонентах.
134 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 1 Использование инструмента Move для перемещения объектов GameObject Панель инструментоввверхней частиредактораUnity позволяет выбрать инструментыTransform. Если инструмент Move не выбран, выберите его нажатием соответствующей кнопки. ИнструментMove предоставляет возможность перемещать объектыGameObject в трехмерном про- странствепри помощиманипулятораMove. Вокнепоявляются тристрелки(красная,зеленаяисиняя), а также куб. Это манипуляторMove, который может использоваться для перемещения выделенного объекта по сцене. Перемещайтеуказатель мыши над кубом в центреманипулятора Move — за - метили, как каждая из граней куба подсвечивается при перемещении над нейуказателя мыши?Щелкнитеналевойверхнейгранииперетащите сферу. Сферабудетперемещаться вплоскостиX–Y. Когда вы щелкаете на левой верхней грани куба в середине манипулятора Move, стрелки X и Y подсвечиваются, и вы можете перетаскивать сферу в плоскости X-Y вашей сцены. Попробуйтеперемещать сферу по сцене,чтобы получить представлениео том, какработаетманипу- ляторMove. Щелкните на каждой граникуба и перетащитеобъект по всем трем плоскостям. Обратите вниманиена то, как сферауменьшается приотдалении от вас (а на самом деле от камеры сцены)иуве- личиваетсяприприближении. Манипулятор Move позво- ляет переме- щать объекты GameObject вдоль любой оси или лю- бой плоскости трехмерного пространства вашей сцены. Кнопка в левой части панели инструментов по- зволяет выбирать такие инструменты преоб- разования, как инструмент Move, который вы- водит манипулятор Move в виде стрелок и куба поверх текущего объекта GameObject.
Лабораторный курс Unity No 1 Head First C# Лабораторный курсUnityNo 1 135 В окне Inspector выводятся компоненты GameObject Вовремяперемещениясферыв трехмерном пространственаблюдайте за окном Inspector, расположенным в правой части окна Unity (при использованиимакета Wide).Просмотрите содержимое окнаInspector— вы увидите, что сфера состоит из четырех компонентов с именами Transform, Sphere (Mesh Filter), Mesh Renderer и Sphere Collider. Каждый объектGameObject состоит из набора компонентов, которые предоставляют основные структурныеблокиего поведения;кроме того, каждыйобъектGameObjectсодержиткомпонентTransform,который управляетего местонахождением,поворотом имасштабированием. Чтобы понаблюдать за компонентом Transform в действии,воспользуй- тесь манипулятором Move для перемещения сферы в плоскостиX–Y. Проследите за тем, как при перемещении сферы изменяются числа X иY встрокеPositionкомпонента Transform. Попробуйтепощелкатьна двухдругихграняхкубаманипулятораMove, чтобыперемещатьсферувплоско- стяхX–Z иY–Z .Затем щелкайтена красной,зеленой исиней стрелкахиперетаскивайтесферувдоль осей X, YиZ.Выувидите, что значенияX,Y иZ вкомпонентеTransform изменяются при перемещении сферы. Теперь нажмите клавишуShift, чтобы превратить куб всередине манипулятора вквадрат. Щелкните на квадрате иперетащитеуказательмыши, чтобыпереместитьсферувплоскости, параллельной камере сцены. Послетогокаквывдоволь поэкспериментируетесманипуляторомMove, воспользуйтесь контекстным меню компонента Transform для сброса компонента к значениям по умолчанию. Щелкните на кнопке конте кстно го м еню вверхнейчасти панелиTransform ивыберите изменю команду Reset. Позиция сферы возвращаетсяк координатам [0,0,0]. Если вы случайно снимете выделение с объекта GameObject, просто щелкните на нем повторно. Если объект не виден в сцене, его можно выделить в окне Hierarchy, в котором перечислены все объекты GameObject в сцене. Когда вы переключаетесь на макет Wide, окно Hierarchy располагается в левом нижнем углу редактора Unity. Воспользуйтесь контекстным меню для сброса компонента. Чтобы вызвать кон- текстное меню, щелкните либо на кнопке с тремя точками, либо правой кнопкой мыши в любой точке верхней строки пане- ли Transform в окне Inspector. А вы заметилисетку в трехмерном пространстве? Удерживайте клавишу Ctrl во время перетаскивания сферы. В этом случае перемещаемый объект GameObject привязывается к сетке. Вы увидите, что числа в компоненте Transform изменяются с целыми приращениями (вместо малых дробных приращений).
136 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 1 Добавление материала к объекту GameObject Unity использует материалы для предоставления цветов, узоров, текстур и другихвизуальных эффектов. Ваша сфера сейчас выглядит довольно уныло — трехмерный объект отображается впростомбелом цвете. Попробуем придать ей видбильярдного шара. Выделите сферу. Когдасферабудет выделена, ее материалотображаетсяввиде компонента вокнеInspector: Чтобы сфера выгляделаболее интересно, добавимтекстуру —простойграфическийфайл, которыйнакладываетсяна трехмерныйобъект(так,какеслибывы напечаталиизображе- ние на листерезины изавернули внего объект). Перейдите на нашу страницу текстур на GitHub. Перейдите по адресу https://gitgub.com/head-first-csharp/fourth-edition и щелкните на ссылке Billiard BallTextures,чтобыпросмотреть папку, содержащуюполныйнаборфайловтекстур бильярдного шара. Загрузите текстуру бильярдного шара с «восьмеркой». Щелкнитенафайле8BallTexture.png,чтобы просмотреть текстурушара с «восьмеркой». Этообычныйграфическийфайл1200×600 вформатеPNG,которыйвы можете открыть в своей любимойпрограммепросмотра графическихфайлов. Загрузитефайл впапку на своем компьютере. (Чтобы сохранитьфайл,щелкните правой кнопкоймыши накнопкеDownoad иливоспользуйтесь кнопкой Download, чтобы открытьфайл, а затем сохранить его, — конкретный способ зависит от браузера.) 1 2 3 Мы спроектиро- вали этот графи- ческий файл так, чтобы он был по- хож на бильярдный шар с «восьмер- кой», когда Unity «заворачивает» в него сферу.
Лабораторный курс Unity No 1 Head First C# Лабораторный курсUnityNo 1 137 Импортируйте текстуру в свой проект Unity. Щелкнитеправой кнопкоймышина папкеAssetsвокнеProject,выберитекоманду Import New Asset... и импортируйтефайл текстуры. Теперь текстура должна по- являтьсяв списке, когда вы щелкаете на папкеAssets вокне Project. Чтобы импортировать новый ресурс, вы щелк- нули правой кнопкой мыши на папке Assets в окне Project, поэтому текстура будет добав- лена в эту папку. Добавьте текстуру к сфере. Теперь необходимо взять текстуруи «завернуть» в неесферу.Щелкнитена тексту- ре8BallTexture в окнеProject,чтобы выделить ее. Послетого как текстура будет выделена, перетащите ее на сферу. 4 5 Теперь сфера напоминает бильярдный шар. Обратитесь кокнуInspector, в которомотображаетсяструктураобъекта GameObject.В немпоявилсяновыйкомпонент материала:
138 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 1 Я изучаюC# для работы, а не для того, чтобы создавать видеоигры. Зачем мне отвлекаться на Unity? Unity помогает действительно хорошо усвоить C#. Программирование —этонавык, и чембольше практикиувасбудет внаписании кода C#, тем лучше выбудете программировать. Вот почемумыпроектировали лабораторные работы Unity в этой книге специально для того, чтобы помочь вам вотработкенавыковC# иукрепитьвашуровеньвладения инструментами и концепциями C#, представленными вкаждой главе. Чембольше кода C# вы напишете, темлучшеувасбудетполучаться, иэто действительноэффективный способ стать квалифицированным программистомC#. Нейробиологияутверж- дает, что мы более эффективноучимсяприэкспериментировании, поэтомумы разработалилабораторные работыUnity сбольшим количеством вариантов и экспериментов, а также приводим рекомендацииотносительно того, как про- явить творческие наклонностипривыполнениикаждойлабораторнойработы. Ночто еще важнее, Unity помогает закрепить важные концепции и приемы C# в вашем мозгу.Во время изучения нового языка программирования очень полезно видеть,как работаетязык наразных платформахи технологиях. Вот почему восновной материал главы включаютсякак консольные приложения, такиприложенияWPF, авнекоторыхслучаяходинитотжепроектдажестро- итсясприменениемобеихтехнологий.ДобавлениеUnityпредоставляет третье направление, котороеможетдействительноускорить ваше пониманиеC#. Расширение GitHub for Unity(https://unity.github.com) позволяет сохранять ваши проекты вUnity. Вот как это делается: • Установка GitHub for Unity: откройте страницу https://assetstore.unity.com и добавьте GitHub for Unity к ресурсам. Вернитесь к Unity, выберите команду Package Manager в меню Window, выберите расширение GitHub for Unity из категории MyAssets и импортируйте его. GitHub нужно будет импортировать в каждый новый проект Unity. • Сохранение изменений в репозитории GitHub: выберите команду GitHub из меню Window. Каждый проект Unity хранится в отдельном репозитории, связанном с учетной записьюGitHub, поэтому щелкните на кнопке Initialize, что- бы инициализировать новый локальный репозиторий (вам будет предложено ввести учетные данные для входа на GitHub).Щелкните на кнопке Publish, чтобы создать новый репозиторий для вашей учетной записи GitHub вашего проекта. Каждый раз, когда вы захотите сохранить изменения на GitHub, перейдите на вкладку Changes в окне GitHub, выберите вариант All, введите краткое описание сохранения (подойдет любой текст) и щелкните на кнопке Commit at в нижней части окна GitHub. Затем щелкните на кнопке Push(1) в верхней части окна GitHub, чтобы со- хранить изменения на GitHub. Также для резервного копирования и распространения ваших проектов Unity можно воспользоваться сервисом Unity Collaborate, который позволяет публиковать проекты в облачном хранилище. Ваша учетная запись Unity Personal бес- платно получает 1 Гбайт облачного хранилища; этого достаточно для всех проектов лабораторныхработ Unity в этой книге. Unity даже будет отслеживать историю вашего проекта (она не учитывается в хранилище). Чтобы опубликовать свой проект, щелкните на кнопке Collab( )напанелиинструментов, после чего щелкните на кнопке Publish. Та же кнопка используется для публикации обновлений. Чтобы просмотреть список опубликованных проектов, пере- йдите по адресу https://unity3d.com , просмотрите свою учетную запись по соответствующей ссылке, а затем щелкните на ссылке Projects на странице со сводкой учетной записи.
Лабораторный курс Unity No 1 Head First C# Лабораторный курсUnityNo 1 139 Вращение сферы ЩелкнитенаинструментеRotateна панелиинструментов. КлавишиQ,W,E,R,T иY могут использо- ваться для быстрого переключения между инструментамиTransform — при помощи клавиш E иW вы будете переключаться между инструментамиRotate и Move. Щелкнитенасфере. Unity выводит каркасную модель сферы —манипуляторRotatec красным,синимизеле- нымкругом. Щелкнитена красном кругеиперетащите его,чтобы повернуть сферупоосиX. Щелкнитеи перетащите зеленый и синий круги, чтобы выполнить поворот по осям Y и Z. Внешний белый круг поворачивает сферу вокруг оси, исходящей из камеры Scene. Проследите за изменением чисел Rotation вокнеInspector. Откройте контекстное меню с панели Transform в окне Inspector. Щелкните на ссылкеReset также,какэтоделалось ранее. ВсесодержимоекомпонентаTransform сбрасывается дозначений по умолчанию — в данномслучаеуглы поворота сферы возвращаютсяк [0,0,0]. Эти команды контекстного меню используются для сброса позиции и углов поворота объекта GameObject. Щелкните на кнопке с тремя точками (или щелк- ните правой кнопкой мыши в любой точке заголовка панели Transform), чтобы открыть контекстное меню. Команда Reset в начале меню возвращает компонент к значениям по умолчанию. . . . 1 2 3 Чтобы сохранить сцену прямо сейчас, выполните команду File>>Save или нажмите Ctrl+S / S. Сохраняйтесь пораньше, сохраняйтесь почаще! Окна икамеры легко возвращаются к зна­ чениямпо умолчанию. Если вы изменили представление Scene так, что сфера не видна, или есливыперетащилиокна изпривыч- ного положения, просто восполь- зуйтесьраскрывающимся списком в правом верхнем углу, чтобы вер­ нуть редактор Unity к макетуWide. При этом происходит сброс макета окна, а камера сцены возвращается кпозициипо умолчанию. РЕ Л А К С
140 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 1 Перемещение камеры сцены инструментом Hand и манипулятором Scene Колесо мыши или прокрутка с сенсорной панели используются для увеличения/уменьшения изображе- ния,а такжепереключениямеждуманипуляторамиMoveиRotate. Обратитевнимание:размеры сферы приэтом изменяются, но размеры манипуляторовостаются неизменными. ОкноSceneвредакторе по- казываетпредставление из виртуальнойкамеры,а функцияпрокруткиприближает иотдаляеткамеру. Нажмите клавишу Q, чтобы выбрать инструмент Hand, или выберите этот инструмент на панели ин- струментов. Указатель мыши принимает вид руки. Инструмент Hand выполняет панорамирование сцены посредством изменения позиции и поворота камеры сцены. Пока инструмент Hand остается выбранным, щелчок в любой точке осуществляет па- норамирование. Щелкните и перетащите инструмент Hand по сцене, чтобы пано- рамировать камеру. Удерживайте клавишу Alt (или Option на Mac) во время перетаскивания ин- струмента Hand, чтобы поворачивать камеру сцены вокруг центра сцены. ПокаостаетсявыбранныминструментHand, камерусценыможнопанорамировать— щелкнитеипере­ тащите указатель мыши. Также можно поворачивать камеру,удерживая клавишуAlt(илиOption) при перетаскивании.Колесо мышииспользуетсядляприближения/отдалениякамеры. Удерживаниеправой кнопкимыши позволяет произвольно перемещаться(«летать»)по сценеприпомощи клавишW-A -S-D . Поворачивая камерусцены, следите за манипуляторомSceneвправом верхнемуглуокнаScene.Манипу- лятор Scene всегда отображаеториентацию камеры — следите за тем, когда вы используете инструмент Hand для перемещения камеры сцены. Щелкайте на конусах X,Y иZ, чтобы привязать камерукоси. ВруководствеUnity приведены полезные советы по навигации в сценах: https://docs.unity3d.com/2020.1/Documentation/Manual/SceneViewNavigation.html. Щелкайте на конусах ма- нипулятора Scene, чтобы привязать камеру к оси. Перетаскивайте их, чтобы поворачивать камеру. Если удерживать нажатой клави- шу Alt (или Option на Mac) во время перетаскивания, инструмент Hand заменяется изображением глаза, а представление поворачивается относительно центра окна.
Лабораторный курс Unity No 1 Head First C# Лабораторный курсUnityNo 1 141 Когда вы щелкаете на объекте GameObject Directional Light в окне Hierarchy, в окне Inspector выводится перечень его компо- нентов. Их всего два: компонент Transform, определяющий его позицию и углы поворо- та, и компонент Light, который непосред- ственно излучает свет. Щелкните на значке Help любого компо- нента, чтобы открыть соответствующую страницу руководства Unity. В: Мне все еще не совсем понятно, что такое «компонент». Что он делает и чемотличаетсяот объекта GameObject? A: ОбъектGameObject сам по себе практически ничего неделает. В дей- ствительности GameObject только служитконтейнеромдля компонентов. Когда вы использовалименюGameObjectдлядобавления сферыв сцену,редактор Unity создал новый объект GameObject и добавил в него все компоненты, определяющиесферы,включая компонентTransformдля позиционирования, поворотов имасштаба;компонентMaterial,которыйокрасил сферу в простой белыйцвет; и еще несколько компонентов, которыеопределяют егоформу и помогают игре определить, когда он сталкивается с другимиобъектами. Именно э т и комп онен ты ф ормиру ют сфе ру. В: Означает ли это, что я могу добавить любой компонент в GameObject, и тот получит соответствующее поведение? О: Да,именнотак. КогдаредакторUnityсоздавал нашу сцену,он добавил дваобъектаGameObject:одинназывалсяMainCamera,адругой —Directional Light.Еслищелкнуть на объектеMainCamera в окнеHierarchy, вы увидите, что он состоит из трех компонентов: Transform, Camera иAudio Listener. Еслизадуматься, то камерадолжнаделать именно это: где-то находиться, получать визуальную информацию и аудио. ОбъектGameObjectDirectional Lightсодержит всего два компонента:Transform и Light, который освещает другие объекты GameObject в сцене. В: Если я добавлю компонент Lightк любому объекту GameObject, он становится источником света? О: Да! Источником света становится любой объект GameObject с ком- понентом Light.Если щелкнуть на кнопке AddComponent в нижней части окна Inspector и добавить компонент Lightк бильярдному шару, он начнет излучать свет. Еслидобавить к сцене еще один объект GameObject, то он будетотражать этот свет. В: Вы как-то слишком осторожно выражаетесь. Почему вы говорите об излучении и отражении света? Почемубы просто не сказать, что он светится? О: Потому чтообъектGameObject, излучающийсвет, исветящийся объект GameObject — совсем не одно итоже. Еслидобавить компонентLight кшару, он начнет излучать свет, но внешний вид его не изменится, потому чтоLight влияет только на другиеобъекты GameObject, которые излучают свет. Если вы хотите, чтобы объект GameObject светился, необходимо сменить его ма- териал или использовать другой компонент, влияющий на его визуализацию. часто Зада вае мые вопросы
142 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 1 ¢ Представление Scene — основное интерактивное представление мира, который вы создаете. ¢ Манипулятор Move перемещает объекты по сцене. МанипуляторScale позволяет изменять масштаб объ- ектов GameObject. ¢ Манипулятор Scene всегда отображает ориентацию камеры. ¢ Unity использует материалы для определения цветов, узоров, текстур и другихвизуальных эффектов. ¢ Некоторые материалы используют текстуры(графиче- ские файлы, наложенные на фигуры). ¢ Декорации, персонажи, элементы окружения, камеры и источники света строятся из объектовGameObject. ¢ Объекты GameObject — фундаментальная разновид- ность объектов вUnity, а компоненты являются основ- ными структурными элементами их поведения. ¢ Каждый объект GameObject содержит компонент Transform, определяющий его позицию, углы поворота и масштаб. ¢ Окно Project отображает представление ресурсов ва- шего проекта, включая сценарии C# и текстуры, в виде иерархии папок. ¢ В окне Hierarchy представлен список всех объектов GameObject в сцене. ¢ GitHub for Unity (https://unity.github.com) упрощает со- хранение проектовUnity в GitHub. ¢ Unity Collaborate также позволяет создавать резерв- ные копии проектов в бесплатном облачном простран- стве, прилагаемом к учетной записи UnityPersonal. КЛЮЧЕВЫЕ МОМЕНТЫ Проявите фантазию! Мысоздали эти лабораторныеработы Unity, чтобы предоставить вам платформу для самостоятельных экспериментов с C#, потому что это самый эффективный способ стать хорошим разработчикомC#. В кон- це каждой лабораторнойработы Unity мы будем приводить несколько предложенийотносительно того,что вы можете попробовать сделать самостоятельно. Не жалейтевремении поэкспериментируйтесовсем, что вы узнали,преждечем переходить к следующей главе: Ì Добавьтеещенесколькосфер ксвоейсцене. Попробуйтеисполь- зовать другие текстуры бильярдных шаров. Их можно загрузить из той же папки,из которойвызагрузилифайл8BallTexture.png. Ì Попробуйте добавить другие фигуры: выберите в меню GameObject>>3D Object варианты Cube, Cylinder или Capsule. Ì Поэкспериментируйтес различными изображениямивкачестве текстур.Посмотрите, что произойдет с фотографиями людей или пейзажамиприиспользованииих для созданиятекстуридо- бавления к различным фигурам. Ì Сможете ли вы создать интересную 3D-сцену изфигур, текстур иисточниковсвета? Чем больше кода C# вы напишете, тем лучше у вас будет по- лучаться, и это дей- ствительно эффек- тивный способ стать квалифицированным программистом C#. Мы создали эти ла- бораторные работы Unity, чтобы предо- ставить вам плат- форму для практики и экспериментов. Когда вы будете готовы перейти к следующей главе, не забудьте сохранить проект, потому что мы вернемся к нему в следующей лабора- торной работе. Unity предложит вам сохра- нить сцену при выходе из редактора.
Ориентируемся на объекты 3 Написание осмысленного кода Каждая написанная вами программа решает некоторую задачу. Когда вы пишете программу, всегда желательно заранее подумать, какую задачу долж- на решать ваша программа. Вот почему объекты приносят такую пользу. Они позволяют сформировать структуру кода в соответствии с решаемой задачей, чтобы вы могли тра- тить время на задачу, над которой работаете, не отвлекаясь на механику написания кода. Если вы правильно используете объекты (и действительно хорошо продумали их при про- ектировании), получившийся код будет интуитивно понятным, будет легко читаться и из- меняться. ...и поэтомумойобъект МладшийБратсодержит метод СъестьКозявку, а его поле ПахнетГрязнымиПеленками переходит в true. Я маме скажу!
144 глава 3 экономия и повторное использование Если код полезен, он используется повторно Разработчики стремились к повторному использованию кода с первых дней про- граммирования, и это вполне понятно.Если вынаписаликлассдляодной программы и у вас имеется другаяпрограмма, для которой нуженкод,делающийто же самое, будет вполнелогично повторно использовать тот же класс вновойпрограмме. namespace Pets { public class Dog { public void Bark() { // команды } } public class Cat { public void Meow() { // другие команды } } } Pets.cs Pr og ram. cs Pet M anag erA pp Pet s.cs Mai nW in do w.xaml MainWindow.xaml.cs Pet Tr ackerW pf A pp Pet s.cs Мы построили клас- сы Dog и Cat для кон- сольного приложения PetManagerApp... Так как мы разместили классы в пространстве имен Pets, доста- точно скопировать файл в новый проект и добавлять команду «using Pets» каждый раз, когда возникнет необходимость в классах Dog и Cat. . ..но ока зало сь , что точно такие же классы понадобятся в приложении WPF PetTracker, поэтому мы использовали их повторно.
дальше 4 145 ориентируемся на объекты Некоторые методы получают параметры и возвращают значение Вывидели методы, которые выполняют конкретные операции (как,например, метод SetUpGame в главе 1, которыйвыполняет подготовку вашейигры).Нометодыспособны набольшее: онимогут использовать параметры для получения ввода, что-то сделать с полученнымиданными, а затем сгенерировать выход- ные данные ввозвращаемом значении, которое можетбыть использованокомандой, вызвавшейметод. Пар аметры получить входныеданные Метод что-то делает Возвращ аемо е зна чени е возвращает выходныеданные Параметры представляют собой значения, используемые методом в качестве входных данных. Они объявляются как переменные, включаемые в объявление метода (в круглыхскобках). Возвращаемое значениевычисляетсяилигенерируетсявнутриметода ивозвращается команде,вкоторойбылвызван метод. Тип возвращаемого значения(например,stringилиint)называется возвращаемым типом.Если методимеет возвращаемыйтип,то ондолженсодержать команду return. Пример метода с двумяпараметрамиint ивозвращаемымтипом int: int M ultiply(int fact or1, int factor2 ) { in t product = fact or1 * factor2; re turn product; } Методполучает два параметра сименамиfactor1 и factor2. Ониспользует операторумножения*для вычисления результата,который возвращается ключевым словом return. Этоткод вызывает метод Multiply и сохраняет результат в переменной с именем area: int heigh t = 179; int width = 83; int area = Multiply(heigh t, width); Методам можно передать непосредственные значения (например, 3 и 5 — Multiply(3, 5)), но при вызове методов также можно использо- вать переменные. При этом имена перемен- ных могут отличаться от имен параметров. Команда return передает значе- ние команде, вызвавшей метод. Сейчас мыначнемсоздавать методы, возвращающие значения, поэтому самое времянаписать кодивоспользоватьсяотладчиком длятого, чтобы действительно понять, как работает командаreturn. Ì Чтопроизойдет, когда методзавершит выполнение всехсвоихкоманд?Посмотрите сами—от - кройтеодну изпрограмм, написанных донастоящего момента, установитеточкупрерывания внутри метода и выполните метод впошаговом режиме. Ì Когда будет выполнена последняя команда в методе, управление возвращается команде, из которойметодбыл вызван,и выполнение программыпродолжается со следующейкоманды. Ì Методтакже может содержать команду return, котораязаставляет методнемедленно вернуть управлениебезвыполнения другихкоманд. Попробуйтевключить дополнительную команду returnв середину метода,послечего продолжитевыполнениевпошаговом режиме. Возвращает- ся тип int, поэтому ме- тод должен во звр а щать значение int ком ан дой return. Метод получает два параметра int с именами factor1 и factor2. Они рас- сматриваются как переменные int. Сделайте это!
146 глава 3 построим приложение Программа для выбора карт Впервой программе этой главы мы построим консольное приложение .NET Console с именем PickRandomCards, которое позволяет выбирать случайныеигровыекарты. Структураприложения вы- гляд ит та к: PickRandomCards PickSomeCards RandomValue() RandomSuit() RandomValue if ... return RandomSuit if ... return Ca rdPi cker Main PickRandomCards() PickRandomCards P rogr am Когда вы создаете консольное приложе- ние в Visual Studio, в пространство имен с именем, совпадающим с именем проек- та, добавляется класс с именем Program. Класс содержит метод Main, который становится точкой входа. Мы добавим еще один класс CardPicker с тремя методами. Метод Main будет вы- зывать метод PickSomeCards нового класса. Метод PickSomeCards будет использовать строковые значения для пред- ставлениякарт. Есливыхотите получить пятькарт, вызовбудет выглядеть так : str ing[] cards = Pi ckSom eCard s(5); Переменнаяcards имееттип, которыйвам пока неизвестен. Квадратные скобки[] означают, что это массивстрок. Массивы позволяют использо- вать одну переменную для хранения многих значений— вданном случае строкс игровымикартами. Пример массива строк, которыйможетбыть возвращен методом PickSomeCards: { "10 of Diamonds", " 6 of Clubs", "7 of Spades", " Ace of Diamonds", " Ace of Hearts" } Послетогокакмассивбудет сгенерирован, воспользуйтесь циклом foreach для вывода его содержимого на консоль. Массив из пяти строк. Наше приложение будет создавать такие массивы для представления случайно выбранных карт.
дальше 4 147 ориентируемся на объекты Создание консольного приложения PickRandomCards Воспользуемся тем, что узнали в этой главе, и создадим программу для выбора нескольких случайных карт. Откройте Visual Studio и создайте новый проект консольного приложения с именемPickRandomCards. В вашу программубудетвходить класс с именем CardPicker. Диаграмма класса, на которой обозначено его имя и методы,выглядиттак: C ard Picker Pi c kS omeCar ds RandomS uit RandomVal ue Диаграмма класса представляет собой прямоугольник, в верхней части которого приведено имя класса, а внизу приведен список методов. КлассCardPicker содержит три метода с именами PickSomeCards, RandomSuit и RandomValue. Щелкнитеправой кнопкой мыши на проекте PickRandomCards в окне Solution Explorer и выберите команду Add>>Class... в Windows(илиAdd>>New Class... в macOS)вконтекстномменю. VisualStudio запрашивает имякласса — выберитеCardPicker.cs . Visual Studio создаетв проекте новыйкласс с именемCardPicker: Новый класс пока пуст — онначинается с объявления class CardPicker и состоитиз парыфигурных скобок,в которых ничего нет. Добавьте новый метод сименем PickSomeCards. Новый класс должен выгляд еть та к: class CardPicker { public static string[] PickSomeCards(int numberOfCards) { } } Если вы тщательно ввели это объявле- ние метода точно так, как оно приведено здесь, под PickSomeCards должна появиться красная волнистая линия. Как вы думаете, что это значит? Обязательно включи- те ключевые слова public и static. Они будут описа- ны позднее в этой главе. Сделайте это!
148 глава 3 return немедленно возвращает управление МетодPickSomeCardsдолженсодержать команду return; добавьте ее. Введите оставшийсякодметода — послетого, какв немпоявляетсякомандаreturn, возвращающаястроковыймассив, ошибка исчезает: class CardPicker { public static string[] PickSomeCards(int numberOfCards) { string[] pickedCards = new string[numberOfCards]; for (int i = 0; i < numberOfCards; i++) { pickedCards[i] = RandomValue() + " of " + RandomSuit(); } return pickedCards; } } Сгенерируйтенедостающие методы. Теперь в коде появляются другиеошибки,потому что в нем нет метода RandomValue или RandomSuit. Сгенерируйте эти методы так, как это было сделано вглаве1.Используйтекнопкубыстрыхдействийна левом полередактора кода:еслищелкнуть на ней,открываетсяменю с командами генерированияобоих методов: Сгенерируйтеметоды. В класседолжныпоявиться методы RandomValue иRandomSuit: class CardPicker { public static string[] PickSomeCards(int numberOfCards) { string[] pickedCards = new string[numberOfCards]; for (int i = 0; i < numberOfCards; i++) { pickedCards[i] = RandomValue() + " of " + RandomSuit(); } return pickedCards; } private static string RandomValue() { throw new NotImplementedException(); } private static string RandomSuit() { throw new NotImplementedException(); } } 1 2 Завершение метода PickSomeCards Когда в методе появляется команда return, которая возвращает значение с типом, соответствующим возвращаемому типу метода, красная волнистая ли- ния исчезает. Для генерирования методов была ис- пользована IDE. Если методы следуют в другом порядке, это нормально — порядок методов в классе роли не игр а ет. Теперь сделайте это!
дальше 4 149 ориентируемся на объекты Используйте команду return для построения методовRandomSuit и RandomValue. Метод можетсодер- жать более однойкоманды return,ипривыполненииоднойизкомандон немедленно возвращает управление — никакие другиекомандыв методе невыполняются. Нижеприведен пример использования команд return в программе. Допустим, вы строите кар- точную игруивамнужныметодыдлягенерированияслучайныхкарточныхмастейиноминалов. Начнемссозданиягенератора случайныхчиселпоаналогиис тем, которыйиспользовалсявигре с поиском пар в главе1.Добавьте его подобъявлением класса: class CardPicker { static Random random = new Random(); Теперь добавьте в метод RandomSuit код, использующий команды return для остановки выпол- ненияметода сразу жепослеобнаружениясовпадения.Метод Next генератора случайных чисел может получать два параметра: random.Next(1, 5) возвращает число от 1(включительно) до 5 (невключая), другими словами, случайное число от 1 до 4. Он будет использоваться методом RandomSuit для выбора случайной карточной масти: private static string RandomSuit() { // получить случайное число от 1 до 4 int value = random.Next(1, 5); // если это 1, вернуть строку Spades if (value == 1) return "Spades"; // если это 2, вернуть строку Hearts if (value == 2) return "Hearts"; // если это 3, вернуть строку Clubs if (value == 3) return "Clubs"; // если выполнение продолжается, вернуть строку Diamonds return "Diamonds"; } А вотметод RandomValue, генерирующий случайный номинал карты. Попробуйтеразобраться втом, каконработает: private static string RandomValue() { int value = random.Next(1, 14); if (value == 1) return "Ace"; if (value == 11) return "Jack"; if (value == 12) return "Queen"; if (value == 13) return "King"; return value.ToString(); } 3 Команда return заставляет метод немедленно пре­ рвать выполне­ ние и вернуться к команде, из которой он был вызван. Мы добавили коммен- тарии, чтобы объ- яснить, что же здесь происходит. А вы заметили, что метод возвращает value.ToString(), а не просто value? Дело в том, что value является переменной int, аметод RandomValueбыл объявлен со строковым возвращаемым типом, поэтому value необходимо преобразовать в строку. Добавление .ToString() к любой переменной или значению преобразует их в строку.
150 глава 3 к ласс го тов , остается з аверши ть пр ил ож ение Готовый класс CardPicker Нижеприведенкод готового класса CardPicker. Ондолженпринадлежать пространству имен,соответ- ствующему именивашего проекта: class CardPicker { static Random random = new Random(); public static string[] PickSomeCards(int numberOfCards) { string[] pickedCards = new string[numberOfCards]; for (int i = 0; i < numberOfCards; i++) { pickedCards[i] = RandomValue() + " of " + RandomSuit(); } return pickedCards; } private static string RandomValue() { int value = random.Next(1, 14); if (value == 1) return "Ace"; if (value == 11) return "Jack"; if (value == 12) return "Queen"; if (value == 13) return "King"; return value.ToString(); } private static string RandomSuit() { // получить случайное число от 1 до 4 int value = random.Next(1, 5); // если это 1, вернуть строку Spades if (value == 1) return "Spades"; // если это 2, вернуть строку Hearts if (value == 2) return "Hearts"; // если это 3, вернуть строку Clubs if (value == 3) return "Clubs"; // если выполнение продолжается, вернуть строку Diamonds return "Diamonds"; } } Мы добавили комментарии, чтобы помочь вам понять, как работает метод RandomSuit. Попробуйте добавить в метод RandomValue аналогичные ком- ментарии, объясняющие, как он работает. Ключевые слова public и static использовались при добавлении метода PickSomeCards. Visual Studio оставляет static при генерировании метода, но объявляет их с ключевым сло- вом private, ане static. Как вы думаете, что делают эти ключевыеслова? Мозговой штурм Мыеще не говорили о полях... почти. Класс CardPicker содер- жит поле с именем random. Поляуже встречались вам в игре с поиском пар из главы 1,но мы с ними ещене работа- ли. Небеспокойтесь — поля иключевое слово static будут намного подробнее рассмотрены в этой главе. РЕ Л А К С Статическое поле с именем "random", используемое для генерирования слу- чайных чисел.
дальше 4 151 ориентируемся на объекты Теперь ваш классCardPicker содержит метод для выбора случайных карт, и у вас появилось все необходимое для завершения консольного приложения, для чего необходимо завершить ме- тод Main. Нам понадобится лишь несколько полезных методов, при помощи которых консольное приложение сможет получить данные от пользователя, и воспользоваться ими для выбора карт. Полезный метод 1: Console.Write Метод Console.Write вам уже известен. Его родственник Console.Write выводит текст на консоль, но не добавляет разрыв строки в конце. Он используется для вывода сообщения для пользователя: Console.Write("Enter the number of cards to pick: "); Полезный метод 2: Console.ReadLine Метод Console.ReadLine читает строку текста и возвращает строку.С помощью этого метода пользователь сможет указать программе, сколько карт нужно выбрать: string line = Console.ReadLine(); Полезный метод 3: int.TryParse Метод CardPicker.PickSomeCards получает параметр int. Входные данные, по - лученные от пользователя, представляют собой строку, которую необходимо как-то преобразовать в int. Для этой цели используется метод int.TryParse: if (int.TryParse(line, out int numberOfCards)) { // этот блок выполняется в том случае, если строка МОЖЕТ БЫТЬ преобразована в int // значение, сохраняемое в новой переменной, называется numberOfCards } el se { // этот блок выполняется, если строка НЕ МОЖЕТ БЫТЬ преобразована в int } Все вместе Ваша задача — взять все три новых фрагмента и объединить их в новом методе Main консольного приложения. Измените файл Program.cs и замените строку с выводом «Hello World!» в методе Main кодом, который решает следующие задачи: Ì Метод Console.Write запрашивает у пользователя количество выбираемых карт. Ì Метод Console.ReadLine читает строку ввода в строковую переменную с именем line. Ì Метод int.TryParse пытается преобразовать ее в переменную типаint с именем numberOfCards. Ì Есливвод преобразуется в значение int, используйте свой классCardPickerдля выбора количества карт, задан- ного пользователем: CardPicker.PickSomeCards(numberOfCards). Используйте переменную string[]длясохранения результатов, а затем воспользуйтесь циклом foreach, чтобы вызвать Console.WriteLine для каждой карты в массиве. Вернитесь к главе 1, чтобыувидеть примерциклаforeach — он будет использоваться для перебора всех элементов массива. Первая строка цикла:foreach(string cardinCardPicker.PickSomeCards(numberOfCards)). Ì Если ввод не может быть преобразован, используйте Console.WriteLine для вывода сообщения, в котором говорится, что число недействительно. Упражнение Покавы работаете над методом Main программы, обратите внимание на его возвращаемый тип. Как вы думаете, что здесь происходит? В главе 2 метод intTryParse использовался в обработчике события TextBox, чтобы он допу- скал ввод только число- вых данных. Задержитесь на минуту и вспомните, как работает этот об- работчик событий.
152 глава 3 отладчик поможет найти ответ Ниже приведен метод Main вашего консольного приложения. Он запрашивает у пользова- теля количество выбираемых карт, пытается преобразовать полученную строку в int, после чего использует метод PickSomeCards из класса CardPicker для выбора соответствующего количе- ства карт. PickSomeCards возвращает все выбранные карты в виде массива строк, поэтому для вывода всех карт на консоль используется цикл foreach. static void Main(string[] args) { Console.Write("Enter the number of cards to pick: "); string line = Console.ReadLine(); if (int.TryParse(line, out int numberOfCards)) { foreach (string card in CardPicker.PickSomeCards(numberOfCards)) { Console.WriteLine(card); } } el se { Console.WriteLine("Please enter a valid number." ); } } А вот что выувидите при запуске консольного приложения: Не торопитесь и постарайтесь по-настоящему разобраться в том, как работает эта програм- ма, — вам предоставляется отличная возможность исследовать ваш код в отладчике Visual Studio. Установите точку прерывания в первой строке метода Main, после чего восполь- зуйтесь функцией Step Into (F11) для пошагового выполнения всей программы. Добавьте отслеживание для переменной value и продолжайте следить за ней во время пошагового выполнения методов RandomSuit и RandomValue. Упражнение Решение Этот метод Main за- меняет метод, ко- торый выводит "Hello World!" (этот метод был создан для вас Visual Studio в файле Program.cs). Ваш метод Main исполь- зует возвращаемый тип void, чтобы сообщить C# об отсутствии возвраща- емого значения. Метод с возвращаемым типом void не обязан содержать метод return. Цикл foreach выполняет Console.WriteLine(card) для каждого элемента массива, возвращенного PickSomeCards.
дальше 4 153 ориентируемся на объекты Анна работает над следующей игрой Знакомьтесь:этоАнна, разработчикинди-игр.Еепоследняяигра былараспродана тысячамиэкземпляров,итеперь она начинает работу над следующей. Анна начала работать над прототипами. Она работала над кодом противни- ков-инопланетяндляоднойзахватывающейчастиигры,когда игрокпытается выбратьсяизсвоегоубежища, азахватчикиего ищут. Аннанаписала несколько методов,определяющихповедение врагов: ониобыскиваютпоследнееместо, в котором был замечен игрок, через какое-то время бросаютпоиски, если найтиигрока неудалось, и захватывают игрока, еслиим удалось подобраться слишком близко. SearchForPlay er(); В моей следующей игре игрок защищает город от инопланетного вторжения. if (Spott edPlayer()) { Communicat ePlayerL ocation(); } CapturePlay er();
154 глава 3 разные игры могутработать похожим образом Игра Анны развивается... Люди против инопланетян— классная идея,но Анна не на сто процентов уверена, что ей захочется пойти именно в этом направлении. Она также думает об игре, в которойигроку-капитану нужнобудетуклонятьсяотпиратов. А можетбыть, это будет игра прозомбина заброшеннойферме.В этихтрехслучаяхврагибудут иметь разнуюграфику,ноих поведениеможет определятьсяоднимиитемижеметодами. .. . Как же Анна может упростить свою задачу? Анна неуверена, в каком направлении пойдет игра, поэтому она хочет по- строить несколько прототипов, и все они должны использовать для врагов один и тот же код с методами SearchForPlayer, StopSearching, SpottedPlayer, CommunicatePlayerLocation и CapturePlayer. А выможете предложить хороший способ использования одних и тех же методов для врагов из разных прототипов? Мозговой штурм Наверняка эти методы могут подойти и для других игр.
дальше 4 155 ориентируемся на объекты En emy Sear chF orP l ayer Spot t edP l ayer Communi cat eP l ayer Locat i on St opS ear chi ng Capt ureP l ayer Я поместила все методы поведения врагов в один класс Enemy. Смогу ли я повторно использовать класс в каждом из трех разных прототипов игры? П ротот ипы Прототиппредставляет собой раннюю версию игры, которую можно использовать для игры, тестирования, изуче- ния и улучшения. Прототип может стать чрезвычайно ценным инструментом, упрощающим внесение ранних из- менений.Прототипы особенно полезны тем, что они позволяют быстро поэкспериментировать с разными идеями, прежде чем принимать долгосрочные решения. • Первым прототипом часто становится бумажный прототип: все основные элементы игры раскладываются на листе бумаги. Например, чтобы достаточно много узнать о своей игре, можно нарисовать уровни или игровые области на больших листах, использовать наклейки или карточки для представления разных элементов игры и перемещать их вручную. • Еще одно преимущество прототипов заключается в том, что они позволяют очень быстро перейти от идеи к работоспособной, действующей игре. Больше всего информации об игре (и вообще о любой программе) можно получить, передав рабочий продукт в руки игроков (или пользователей). • Многие игры проходят через несколько прототипов. Так вы получаете возможность опробовать многоразных идей и поучиться на них. Даже если что-то пошло не так , считайте это экспериментом, а не ошибкой. • Построение прототипов — это навык. Как и любой другой навык, он совершенствуется с применением на практике. К счастью, строить прототипы также интересно, и это отличный способ стать сильнее в написании кода C#. Прототипы применяются не только для игр! При построении программы любого типа часто стоит начать с построения прототипа, чтобы поэкспериментировать с разными идеями. Разработка игр... и не только
156 глава 3 построениебумажного прототипа PLAYER 32150 X20 WORLD 3-1 TI ME 182 Построение бумажного прототипа для классической игры Бумажныепрототипыпомогут вам продумать,какдолжна работать игра,ещедо началаее построения, а это сэкономитвам уйму времени. Построение начнется практически мгно- венно — вам понадобится лишь бумага и карандаш. Попробуйте выбрать свою любимую классическуюигру.Платформенные игрыподходятособенно хорошо, поэтомумывыбрали однуиз самыхпопулярных, самыхузнаваемыхклассическихигр...но выможете выбрать любую игру по своему вкусу! Теперь необходимо сделать следующее. Земля, кирпичи и трубы не дви- гаются, поэтому мы нарисовали их на фон овой бумаге. Не суще- ствует жестких правил относи- тельно того, что должно находить- ся на фоне, а что двигаться. Механика прыж- ков игрового персонажа была тщательно спроектирована создателями игры. Моделиро- вание прыжков на бумажном прототипе — полезное учебное упражнение. Когда игрок подби- рает гриб, он уве- личивается в раз- мерах, поэтому мы также нари- совали отдельного маленького персо- нажа на отдельном клочке бумаги. Текст в верхней части экрана называется HUD (Head-Up Display). Нарисуйте Нарисуйтефонна листебумаги. Построениепрототипаначинаетсяс созданияфона. Внашем прототипе земля, кирпичи и трубы не двигаются, поэтому мы нарисовали их на бумаге. Также вверхней части листа добавлен счет, время и прочийтекст. Оторвите маленькие клочки бумаги и нарисуйте подвижные части. В нашем прототипе персонажи,хищноерастение,гриб,огненный цветок имонеткирисуютсяна отдельных листках. Есливынесильныврисовании— ничегострашного!Простонарисуйтесхематичныхчеловечков иприблизительные контурыфигур.В концеконцов,никто неувидитвашего творчества! Немного «поиграйте». Этосамаяинтереснаячасть!Попробуйте смоделироватьдвижение игрока. Перемещайте игрока по странице. Также заставляйте двигаться другихперсонажей. Очень по- лезно несколько минутпровестиза игрой, а затем вернутьсяк прототипу ипосмотреть, сможете ли вы какможно точнеевоспроизвестидвижение. (Поначалу это кажетсянемного странным, но это нормально!) 1 2 3
дальше 4 157 ориентируемся на объекты Похоже, бумажные прототипы пригодятся не только в играх. Уверен, что их можно будет использовать и в других проектах. Да! Бумажный прототип станет отличным первым шагом для любого проекта. Есливыстроите настольное приложение, мобильное приложениеили любой другой проект, укоторого есть пользовательскийинтерфейс, построениебумажногопрототипа станет хорошейотправнойточкой. Иногда приходитсяпостроитьнесколькобумажных прототипов, прежде чемвыначнете понимать, чтокчему.Собственно,именно поэтому мы начали с бумажного прототипа для классической игры... потому, что он наглядно демонстрирует, какстроитьбумажныепрототипы. Построениепрототипов — исклю- чительноважный навыкдля любыхразработчиков, не только для разработчиковигр. Все инструменты и идеи, представ- ленные в разделе «Разработка игр... и не только», относятся к числу важных навыков программирования, которые выходят за рамки простой разработки игр. Тем не менее наш опыт показывает, что их становит- ся проще осваивать после того, как вы сначала опробуете их на играх. В следующем проекте мысоздадим приложениеWPF, которое использует класс CardPicker для генерирования случайного набора карт. В этом письменном упражнении мы построим бумажный прототип нашего приложения, чтобы опро- бовать различные варианты проектирования интерфейса. Начнем срисования контураокнанабольшом листебумагиинадписина меньшем листке. CARD PICKER Теперь нарисуем несколькоразных элементов управления на еще меньших клочкахбу- маги.Перемещайтеихв окнеи экспериментируйте сих совместным размещением.Как вы думаете,какой вариант работает лучше всего? Единственноправильного ответа не существует —любоеприложениеможноспроектировать множествомразныхспособов. 4 OF HEARTS 2 OF DIAMONDS KING OF SPADES ACE OF HEARTS 7 OF CLUBS 10OF SPADES JACK OF CLUBS 9 OF HEARTS 9 OF DIAMONDS 3 OF CLUBS ACE OF SPADES PICK SOME CARDS 12 HOW MANY CARDS SHOULD I PICK? Ваше приложение должно предоста- вить возможность выбора количества карт. Попробуйте нарисовать поле ввода, в котором пользователь мо- жет напрямую ввести число в при- ложении. Также попробуйте использовать ползунок и набор переключателей. А сможете ли вы предложить другие элементы, кото- рые ранее уже использовались для ввода чисел в приложении? Может, раскрываю- щийся список? Проявите фантазию! 1 2 3 4 5 Где-то в окне приложения дол- жен размещаться список с картами и кнопка «Pick some cards». Возьми в руку карандаш
158 глава 3 контейнер содержит другие элементы Следующий шаг: построение WPF-версии приложения для выбора карт Вследующем проекте мы построим приложениеWPF с именем PickACardUI. Оно будет выглядеть примерно так: ВприложенииPickACardUI элементSliderиспользуется для выбора количества случайныхкарт. После выбора количества карт вы щелкаете на кнопке, чтобы приложение выбрало ихи добавило вListBox. Окно будет выглядеть примерно так: Элемент ListBox. Содержит список элементов, которые могут выбираться поль- зователем, — в данном случае список карт. Эле- мент занимает 2 строки и выравнивается по центру столбца сотступами 20. В ячейке располо- жены два элемен- та, Label и Slider. Вскоре мы более внимательно раз- беремся в том, как это работает. Окно делится на две строки и два столбца. Элемент ListBox в правом столбце занимает обе строки. Обработчик события Button будет вызывать метод вашего класса, который возвращает список карт. Все возвращенные карты будут добавляться в ListBox. Мы не будем напоминать вам о том, что проекты следует добавлять в систему упра вле ния верс ия ми, но мы все равно считаем, что вам стоит создать учетную запись GitHub и публиковать под ней все ваши проекты! Версия этого проекта для Mac доступна в приложении «Visual Studio для пользователей Mac». Здесь вы найдете версии всех WPF- проектов из книги для ASP.NET Core со снимками экрана из Visual Studio для Mac. Мы решили использовать ползунок для выбора числа карт. Но конечно, это не единственный вариант построения интер- фейса! Может, вы предложили другой вариант в своем бумаж- ном прототипе? Это нормаль- но! Любое приложение можно спроектировать множеством разных способов, и почти ни- когда не существует однозначно правильного (или ошибочного) решения.
дальше 4 159 ориентируемся на объекты StackPanel ¦ контейнер для наложения элементов ВашеприложениеWPF будет использовать элемент Grid для размещения элементов по аналогии с тем, как это делалось в игре с поиском пар. Но прежде чем переходить к написанию кода, стоит повнима- тельнееприсмотретьсяк двум элементам влевойверхнейячейке сетки: Какжесвязать этиэлементы другс другом?Можно попытатьсяразместить их воднойячейкесетки: <Grid> <Label HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20" Content="How many cards should I pick?" FontSize="20"/> <Slider VerticalAlignment="Center" Margin="20" Minimum="1" Maximum="15" Foreground="Black" IsSnapToTickEnabled="True" TickPlacement="BottomRight" /> < /Gr id> Новэтомслучаеэлементы простобудутперекрываться: Напомощь приходит элементStackPanel.StackPanel являетсяконтейнером— каки Grid, онпредназначен для размещения другихэлементов иобеспечения их правильной позиции в окне. Если Grid позволяет выстраивать своиэлементы по строкам истолбцам,StackPanel выстраивает элементывгоризонтальный или вертикальныйряд. ВозьмемтежеэлементыLabel иSlider, но наэтот раз воспользуемсяStackPanel дляразмещенияихтаким образом, чтобы элемент Label располагался поверх Slider. Обратите внимание на то, что свойства вы- равниванияиотступовбыли перемещены в StackPanel — по центрудолжна выравниватьсясама панель вместес прилегающимик нейотступами: <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20" > <Label Content="How many cards should I pick?" FontSize="20" /> <Slider Minimum="1" Maximum="15" Foreground="Black" IsSnapToTickEnabled="True" TickPlacement="BottomRight" /> < /St ack Pane l> ПрииспользованииStackPanel элементы в ячейке выглядят именно так, как требовалось: Это элемент Label... ...а это элементSlider. Код XAML элемен- та Slider. Он будет более подробно про- анализирован, когда мы займемся по- строением формы. Так должен работать наш проект. Перейдем к его построению!
160 глава 3 тот же класс, другое приложение Повторное использование класса CardPicker в новом приложении WPF Есливынаписаликласс для однойпрограммы, часто бывает возможно использовать то же поведение вдругой программе.Одноиз главныхпреимуществклассов какрази заключается в том, что они упрощают повторное использование кода. Давайте создадим для прило- жения красивыйновыйинтерфейс, но сохраним прежнее поведение за счет повторного использованиякласса CardPicker. Создайте новое приложение WPF с именем PickACardUI. Выполнитетеже действия, что и для игры с поиском пар вглаве1: Ì ОткройтеVisual Studio исоздайте новыйпроект. Ì ВыберитешаблонWPFApp(.NETCore). Ì Присвойте новому приложению имя PickACardUI. Visual Studio создает проект и добавляет в негофайлыMainWindow.xamlиMainWindow.xaml.cs,входящие впространствоименPickACardUI. Добавьте класс CardPicker, созданный для проекта консольного приложения. Щелкнитеправойкнопкой мыши наимени проекта, выберите в менюкомандуAdd>>ExistingItem... Перейдитевпапкус вашимконсольнымприложениеми выберитефайлCardPicker.cs,чтобыдоба- вить егов проект. ВпроектеWPFпоявляетсякопияфайлаCardPicker.csиз консольногоприложения. Измените пространство имен класса CardPicker. Сделайте двойной щелчок на файлеCardPicker.cs в окне Solution Explorer. Файл все еще принад- лежит пространству имен из консольного приложения. Измените пространство имен и при- ведите его в соответствие с именем проекта. Подсказка IntelliSense предложит пространство именPickACardUI — нажмитеклавишу Tab, чтобы согласиться спредложением: Теперь класс CardPicker должен принадлежать пространству имен PickACardUI: namespace PickACardUI { class CardPicker { Поздравляем, вы повторно использовали класс CardPicker! Класс долженпоявиться в окне Solution Explorer, ивы сможетеиспользовать его в кодесвоего приложенияWPF. 1 2 3 Повторите это Мы изменяем пространство имен в файле CardPicker.cs и заменяем его пространством имен, ис- пользованным Visual Studio при создании файлов нового проекта. Это делается для того, чтобы класс CardPicker мог использо- ваться в коде нового проекта.
дальше 4 161 ориентируемся на объекты Использование Grid и StackPanel для формирования макета главного окна В главе1 элементGrid использовался для формирования макета окна игры с поиском пар. Вернитесь к той части главы, в которой создавалась структурасетки,потому что нам предстоитпроделать то же самоедляформирования макета окна нового приложения. Определите строки истолбцы. Выполните действия, описанные вглаве 1, чтобыдобавитьв сет- ку две строки и два столбца. Если всебыло сделано правильно, определения строк и столбцов должны появиться под тегом <Grid> в XAML: <Gr id. RowD efi nit ions > < RowD efi nit ion/ > < RowD efi nit ion/ > </G rid .Row Def ini tion s> <Gr id.C olu mnD efin iti ons > < Colu mnD efi niti on/ > < Colu mnD efi niti on/ > </G rid .Col umn Def init ion s> Добавьте элементStackPanel.Работать спустымэлементомStackPanel в визуальномконструкторе XAMLнемногонеудобно,потому что на нем трудно щелкнуть. Поэтому мы выполним операцию в редакторе кода XAML. Сделайте двойной щелчок на элементе StackPanel на панели инстру- ментов, чтобы добавить пустойэлементStackPanel насетку.Это должно выглядеть примерно так: </Grid.ColumnDefinitions> <StackPanel/> </Grid> </Window> Задайте свойства StackPanel. При двойном щелчке на элементе StackPanel на панели инстру- ментов был добавлен элемент StackPanel без свойств. По умолчанию он находится в левом верхнемуглусетки,поэтому теперь остается настроить выравнивание и отступы. Щелкните на тегеStackPanelвредактореXAML,чтобывыделитьтег.Когда тегбудет выделенвредакторекода, списокего свойствпоявляетсявокне свойств.Выберитережимвертикальногоигоризонтального выравнивания Center и установите отступы 20. Сейчас элементStackPanel в коде XAML должен выглядеть так: <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20" /> 1 2 3 Используйте конструктор Visual Studio для добавления двух строк одинаковой высоты и двух столб- цов одинаковой ширины. Если у вас возникнут проблемы, введите код XAML прямо в редакторе. Чтобы вам было проще пере- тащить элементы с панели инструментов, восполь- зуйтесь кнопкой в правом верхнем углу панели инстру- ментов, чтобы закрепить ее в окне. Когда вы щелкаете на элементе в ре- дакторе кода XAML и просматрива- ете его свойства в окне свойств, код XAML будет обновлен немедленно. Это означает, что все отступы равны 20. Сто- ит заметить, что свойства Margin на панели инструментов содержат набор значений «20, 20, 20, 20» — это означает то же самое.
162 глава 3 завершениеработы над приложением ДобавьтеLabel иSlider вStackPanel.StackPanelявляетсяконтейнером.Когдаэлемент StackPanel несодержит другихэлементов, он не виден вконструкторе, а этоусложняет перетаскиваниена него элементов. К счастью, добавить элементы также просто, как задать его свойства. Щелкните на элементе StackPanel, чтобывыделить его. Пока элементStackPanel остается выделенным, сделайте двойной щелчок на элементе Label на панели инструментов,чтобы поместить новый элементLabel внутри StackPanel. ЭлементLabel отображаетсяв конструкторе, а в редакторе кода XAML появляется тег Label. ЗатемраскройтеразделAll WPF Controls на панелиинструментовисделайтедвойнойщелчок на элементеSlider. Влевойверхнейячейкедолженпоявитьсяэлемент StackPanel, которыйсодержит элемент Label над Slider. ЗадайтесвойстваэлементовLabel иSlider. Теперь, когдаэлементStackPanelсодержитэлементы Label иSlider,остается задать их свойства: Ì Щелкнитена элементеLabel вконструкторе. РаскройтеразделCommon вокне свойств ивве- дите текст How many cards should I pick?, затем раскройтераздел text и назначьте ему размер шрифта 20px. Ì НажмитеклавишуEscape,чтобы снять выделениесLabel,затем щелкнитена элементеSlider вконструкторе, чтобы выделитьего.Воспользуйтесь полемNameв верхней части окнасвойств, чтобы изменить его имя на numberOfCards. Ì Раскройтераздел Layout исбросьтеширинупри помощикнопки с квадратиком( ). Ì РаскройтеразделCommonизадайтесвойствуMaximum значение15,свойствуMinimum— зна - чение 1, свойствуAutoToolTipPlacement— значение TopLeftисвойствуTickPlacement— значение BottomRight.Затем щелкните настрелке( ), чтобыраскрытьразделLayoutсдополнительными свойствами, включаясвойствоIsSnapToTickEnabled.Задайтеему значениеtrue. Ì Сделаем шкалу ползункаболеезаметной.Раскройтераздел Brush в окне свойств ищелкните на большом прямоугольнике справа от Foreground — это позволит вам выбрать основной цвет ползунка. Щелкните на полеR и введите 0, затем также введите 0 в поляхG иB. Поле Foregroundдолжно быть черным,и меткиподползунком должны быть черными. Код XAML должен выглядеть так (если у вас возникнут проблемы с конструктором, просто от- редактируйте XAML напрямую): 1 2 Формирование макета окна приложения Card Picker Сформируйтемакетокнаприложениятак, чтобыэлементыуправлениярасполагались сле- ва, авыбранныекарты —справа.ЭлементStackPanelразмещаетсявлевойверхнейячейке. Данный элемент являетсяконтейнером;это означает,что онсодержит другиеэлементы, какиGrid.Но вместо того чтобывыстраивать элементывячейках,онвыстраиваетихпо вертикали или по горизонтали. После того как вStackPanel будутразмещены элементы Label иSlider,мы добавим элементListBox по аналогиис тем,как это делалось в главе2. Ско нст руи­ руйте это! <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20 "> <Label Content="How many cards should I pick?" FontSize="20"/> <Slider x:Name="numberOfCards" Minimum="1 " Maximum="15 " TickPlacement="BottomRight" IsSnapToTickEnabled="True" AutoToolTipPlacement="TopLeft" Foreground="Black"/> < /St ackP ane l>
дальше 4 163 ориентируемся на объекты Добавьте элемент Button в левую нижнюю ячейку. Перетащите элемент Button с панели ин- струментов в левую нижнюю ячейку сетки, задайте его свойства: Ì Раскройтераздел Common и задайте его свойству Content значение Pick some cards. Ì Раскройтераздел Text изадайтеразмер шрифта 20px. Ì Раскройтераздел Layout. Сбросьтеего отступы, ширину и высоту. Затем задайте режимы вер- тикального и горизонтального выравниванияCenter( и ). Код XAML элемента Button должен выглядеть так: <Button Grid.Row="1" Content="Pick some cards" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center" /> Добавьте элемент ListBox, который заполняет правую половину окна и занимает две строки. Перетащите элемент ListBox в правую верхнюю ячейку и задайте его свойства. Ì ИспользуйтеполеName вверхней части окна свойств,чтобы задать ListBox имяlistOfCards. Ì Раскройтераздел Text изадайтеразмер шрифта 20px. Ì Раскройтераздел Layout. Задайте отступы размером 20, как это было сделано для элемента StackPanel. Проследите за тем, чтобы ширина, высота, горизонтальное и вертикальное вы- равнивание были сброшены. Ì Убедитесь втом, что полеRow содержит значение0, а поле Column — значение 1.Затемзадайте RowSpan значение2,чтобы элементListBoxзанимал весь столбец иохватывал обе строки: Код XAML элемента ListBox должен выглядеть так: <ListBox x:Name="listOfCards" Grid.Column="1" Grid.RowSpan="2 " FontSize="20" Margin="20 ,20,20,20"/> Задайте текст заголовка иразмерокна. Когдавы создаетеновоеприложениеWPF,VisualStudio создает главноеокношириной450пикселовивысотой800 пикселов сзаголовком «MainWindow». Изменимегоразмерыпо аналогии с тем,как это делалось вигрес поиском пар: Ì Щелкнитена строкезаголовка окна в конструкторе,чтобы выделить окно. Ì В разделе Layout задайте ширину 300. Ì В разделе Common введите текст заголовка Card Picker. Прокрутите редактор XAMLи найдите последнюю строку тега Window. Она содержит следующие свойства: Title="Card Picker" Height="300 " Width= "800" 3 44 45 Если свойство содержит значение "20" вместо "20, 20, 20, 20", это нор- мально — фактически это то же самое.
164 глава 3 так работает повторное использование кода ¢ Классы содержат методы, а методы состоят из команд, выполняющих действия. В хорошо спроек- тированных классах используются содержательные имена методов. ¢ Некоторые методы имеют возвращаемый тип, кото- рый задается в объявлении метода. Метод с объявле- нием, начинающимся с ключевого слова int, возвра- щает значение int. Пример команды, возвращающей значение int: return 37. ¢ Если метод имеет возвращаемый тип, он должен содержать команду return со значением, соот - ветствующим возвращаемому типу. Таким образом, если в объявлении метода указан строковый возвра- щаемый тип, этот метод должен содержать команду return, которая возвращает строку. ¢ Как только в методе будет выполнена команда return, программа возвращается к команде, из кото- рой был вызван метод. ¢ Не все методы имеют возвращаемый тип. Метод с объявлением, начинающимся с public void, ни- чего не возвращает. При этом для выхода из мето- да void может использоваться команда return: if (finishedEarly) { return; } ¢ Разработчики часто хотят повторно использовать один код в разных программах. Классы помогают рас- ширить возможности повторного использования кода. ¢ Когда вы выбираете элемент в редакторе XAML, свойства этого элемента можно изменить в окне свойств. КЛЮЧЕВЫЕ МОМЕНТЫ Добавьте обработчик события Click в элемент Button. Код программной части — код C# из файла MainWindow.xaml.cs, связанного с кодом XAML, — состоит из одного метода. Сделайте двойной щелчок на кнопке в конструкторе; IDE добавляет метод Button_Click и назначает его об- работчиком событияClick, какбыло показано вглаве1.Коднового метода: private void Button_Click(object sender, RoutedEventArgs e) { string[] pickedCards = CardPicker.PickSomeCards((int)numberOfCards.Value); listOfCards.Items.Clear(); foreach (string card in pickedCards) { listOfCards.Items.Add(card); } } 46 Запуститесвоеприложение.Припомощиползунка выберитеколичество слу- чайных карт, а затем нажмите кнопку, чтобы заполнить ими списокListBox. Отличная работа! Код C#, свя­ занный с ок­ ном XAML и содержащий обработчики событий, на­ зывается кодом программной части.
дальше 4 165 ориентируемся на объекты Enemy1 Sear chF orP l ayer Spot t edP l ayer Communi cat eP l ayer Locat i on St opS ear chi ng Capt ureP l ayer Enemy2 SearchForPlayer SpottedPlayer C ommunic at eP lay erLoc at ion StopSearching C apt ureP lay er Enemy3 Sear chF orP l ayer Spot t edP l ayer Communi cat eP l ayer Locat i on St opS ear chi ng Capt ureP l ayer . . .н о что, если противников должно быть несколько? Ивсеидет замечательно...ПокаАннанерешает использоватьболее одноговрага, который присутствовалв еераннихпрототипах.Чтоей следуетсделать,чтобы добавить вигрувторого илитретьего врага? Анна может скопировать код класса Enemy и вставить его еще в два файла. Тогда ее программа сможет использовать методы для управления сразу тремяразными врагами. Так что формально код используется повторно... разве нет? Эй,Анна, что скажешь? Прототипы Анны выглядят замечательно... Аннаобнаружила, чтоктобынипреследовал игрокавееигре—ино - планетянин, пират, зомби или злобный клоун, для представления врага можноиспользовать одни итежеметоды ее класса Enemy.Ее игра начинаетпонемногу обретатьформу. В ее словах есть смысл. А если ей потребуется уровень, на ко- тором бегают десятки зомби? Создавать десятки одинаковых классов просто непрактично. Вы шутите? Использование отдельных идентичных классов для разных врагов — ужасная мысль. А если в какой-то момент мне понадобится более трех врагов? Сопровожд ение т рех ко пий одного кода сулит массу проблем. Вомногихзадачах, которые намприходит- ся решать, требуется представить нечто многими разными способами. В данном случае это враг в видеоигре,но могли бы бытьпеснивмузыкальном проигрывателе или контакты в социальной сети. У всех этих данных имеется одна общая особен- ность: всем им требуетсяодинаково рабо- тать с некоторыми данными, сколько бы экземпляров этих данных у них нибыло. Попробуем найтиболееудачное решение. Enemy SearchForPlayer SpottedPlayer Communi c at ePl ay erLoc at ion S t opS earc hing Capt ur eP lay er
166 глава 3 знакомство с объектами n e w E n e m y ( ) n e w E n e m y ( ) О б ъ е к т E n e m y enemy3 Анна может воспользоваться объектами для решения своей задачи ОбъектывC# предназначены для работы с наборами по- хожихсущностей.Аннаможет воспользоватьсяобъектами для того, чтобы запрограммировать свой класс Enemy только один раз и использовать его в программе столько раз,сколькопотребуется. О б ъ е к т E n e m y enemy1 О б ъ е к т E n e m y enemy2 n e w E n e m y ( ) Enemy enemy1 = new Enemy(); enemy1.SearchForPlayer(); if (enemy1.SpottedPlayer()) { enemy1.CommunicatePlayerLocation(); }else{ enemy1.StopSearching(); } Все, что нужно для создания объекта, — ключевое слово new и имя класса. Теперь объект может использоваться в программе! Когда вы создаете объект на базе класса, этот объект содержит все методы, определенные в классе. На уровне с тремя врагами, преследующими игрока, будут одновременно существовать три объектаEnemy. En emy SearchForPlayer SpottedPlayer C ommunic at eP lay erLoc at i on StopSearching C apt ureP lay er
дальше 4 167 ориентируемся на объекты О б ъ е к т H o u s e О б ъ е к т H o u s e О б ъ е к т H o u s e Класснапоминаетпландляпостроенияобъекта. Чтобыпостроить пять одинаковых домовв пригородной зоне, никто нестанет за- казывать у архитектора пять комплектоводинаковых планов. Вы просто используете одинпландля всех пяти домов. Класс используется для построения объектов Объект получает методы от класса Послетогокакклассбудетсоздан,выможетесоздать на его базе сколько угодно объектовкомандойnew. Приэтомкаждыйметод вклассестановится частью объекта. 115 Maple Drive 38 Pine Street 26A Elm Lane Класс определяет свои компоненты по аналогии с тем, как план определяет компоновку дома. Один план может использоваться для строительства любого количества домов; точно так же на базе одного класса можно соз­ дать любое количе­ ство объектов. Ho use Gr owLawn Recei v eDeli v erie s Ac c rueP roper t yTaxes NeedRepai rs Класс House содержит четыре метода, которые могут ис- пользоваться каждым экземпля- ром House.
168 глава 3 об ъекты улучшаю т ваш к од О б ъ е к т H o u s e 115 Maple Drive Новый объект, созданный на базе класса, называется экземпляром этого класса Эк-зем -пляр, сущ. Копия или отдельное вхож- дение чего-либо. Длясозданияобъекта может использоватьсяключевое слово new. От вас потребуетсятолько переменная,ко- торая будет использоваться для обращения к объекту. Классуказываетсякактип дляобъявленияпеременной, так что вместо intилиbool следуетуказать имякласса— например,HouseилиEnemy. Досоздания объекта: так выглядит память вашего компьютера при запуске программы. После создания объекта: теперь в памяти хранится экземпляр класса House. House mapleDrive115 = new House(); В ваш ей кома нде выпол няе т ся команд а ne w. Ключевое слово new выглядит знакомо. Я его уже где-то видел. Да! Вы уже создавали экземпляры в своем коде. Вернитесь кпрограммес поискомпар и найдитеследующую строку кода: Random random = new Random(); Мысоздали экземпляр класса Random,а затем вызвалиметодNext. Теперь просмотрите код класса CardPicker и найдите команду new. Выходит,мыуже давно пользуемсяобъектами! Команда new создает новый объект House и присваива- ет его переменной с име- нем mapleDrive115.
дальше 4 169 ориентируемся на объекты О б ъ е к т E n e m y enemy3 О б ъ е к т E n e m y enemy1 О б ъ е к т E n e m y enemy2 О б ъ е к т E n e m y enemy1 Хорошее решение для Анны (с объектами) Аннаповторно использовала кодкласса Enemy без хлопотс копированием, котороебыпривелок дублированию кода впроекте. Вот каконаэтосделала. for(inti=0;i<3;i++) { Enemy enemy = new Enemy(); enemyArray[i] = enemy; } Она воспользовалась циклом, в котором выполня- лись командыnew для созданияновых экземпляров класса Enemy для текущего уровня, а созданные экземпляры добавлялись вмассив врагов. 2 Анна создала класс Level, который хранит врагов в массивеEnemy сименемenemies, — подобнотому, какмассивы строкиспользовались дляхранениякарт иэмодзис животными. public class Level { Enemy[] enemyArray = new Enemy[3]; 1 Методы каждого экземпляра Enemy вызываются при каждом обновлении кадра для реализации поведения врагов. 3 Объект enemy1 является экземпляром класса Enemy. Эта коман- да использует ключевое слово new для соз- дания объекта Enemy. Эта команда добавляет только что созданный объ- ект Enemy в массив. Ключевое слово new используется для соз- дания массива объектов Enemу по аналогии с тем, как это делалось со строками. Используйте имя класса для объявления массива экземпляров этого класса. foreach (Enemy enemy in enemyArray) { // Код, содержащий вызовы методов Enemy } Enemy SearchForPlayer SpottedPlayer Communi c at ePl ay erLoc at ion S t opS earc hing Capt ur ePl ay er n e w E n e m y ( ) Хм, этот массив находится внутри класса, но за предела- ми методов. Как вы думаете, что здесь происходит? Цикл foreach перебирает содержимое массива объ- ектов Enemy.
170 глава 3 нем но г о секр етно го соуса Минутку! Этой информации даже отдаленно не хватит для построения игры Анны. Верно, не хватит. Однипрототипы игр очень просты, другиеустроены намного сложнее,но сложныепрограммы строятся набазетех жепаттернов,чтоипростые. Программа Анныявляетсяпримеромтого,какбывыиспользова- ли объектывреальной жизни. Иэтот принципприме- нимне только дляразработкиигр!Какуюбыпрограм- мувыни строили, объектыв нейбудутиспользоваться точно так же,как их использовала Анна в своей про- грамме. ПримерАнны— всеголишь отправнаяточка для закрепленияэтойконцепции ввашем мозгу.Мы приведем ещеочень много примеров воставшейся части главы— и эта концепция настольковажна,что мы еще вернемся к ней вбудущих главах. Теория и практика Разужречь зашла о паттернах, существует один паттерн, который мы еще неоднократно увидим в книге. Мы пред- ставляем новую концепцию илиидею (например, объекты) на несколькихстраницах,используя картинкии короткие фрагментыкодадляее пояснения.Этодаетвамвозможность задержаться и попробовать разобратьсявпроисходящем, не отвлекаясь на то, как заставить работать конкретную программу. О б ъ е к т H o u s e 115 Maple Drive Когда в книге вводится новая концепция (например, объек- ты), обращайте особое вни- мание на картинки и фраг- менты кода. House mapleDrive115 = new House();
дальше 4 171 ориентируемся на объекты Итак, теперь вы лучше представляете, как работают объекты. Самое время вер- нуться кклассуCardPickerипоближепознакомитьсясклассом Random,который внем используется. 1. Установите курсор внутри любого метода, нажмите клавишу Enter, чтобы начать новую команду, и введите random. — как только вы введете точку,Visual Studio открывает окноIntelliSense сосписком методов.Каждыйме- тод помечается значком в виде кубика ( ).Мызаполнилинекоторые методы. Допишите отсутствующие методы вдиаграмму класса Random. Random Equals GetHashCode GetType ToString 2.Напишитекодсоздания новогомассиваdouble сименем randomDoubles.Добавьте вмассив 20 значенийdouble в цикле for. Добавляйте только случайные числа с плавающейточкойв интервале от 0.0 (включительно) до 1.0 (невключая). Используйте окно IntelliSense для выбора метода класса Random, которыйдолжен использоваться ввашем коде. Random random = double[] randomDoubles = new double[20]; { double value = } Мы заполни- ли часть кода, включая фи- гурные скобки. Ваша задача — дописать эти команды, а за- тем написать остальной код. Возьми в руку карандаш
172 глава 3 статические компоненты являются общими для всех экземпляров Итак, теперь вы лучше представляете, как работают объекты. Самое время вернуться к классу CardPicker и поближе познакомиться с классом Random, которыйв нем используется. 1. Установите курсор внутри любого метода, нажмите клавишу Enter, чтобы начать новую команду, и введите random. — как только вы введете точку,VisualStudioоткрывает окно IntelliSense сосписком методов.Каждыйме- тод помечается значком в виде кубика ( ). Мы заполнили некоторые методы.Допишите отсутствующие методы в диаграммуклассаRandom. Random Equals GetHashCode GetType ToString 2.Напишитекод созданияновогомассива doubleсименем randomDoubles.Добавьтев массив20 значенийdouble в цикле for. Добавляйте толькослучайные числа с плавающей точкой в интервале от 0.0 (включительно) до 1.0 (не включая).Используйте окно IntelliSense для выбора метода класса Random, который должен использоваться в вашем коде. Next NextBytes NextDouble Окно IntelliSense, кото- рое появляется в Visual Studio, когда вы вводите «random.» в одном из методов CardPicker. Когда вы выбирае- те NextDouble в окне IntelliSense, появляется кр аткая д окум е нта- ция по использованию метода. Random random = new Random(); double[] randomDoubles = new double[20]; for(inti=0;i<20;i++) { double value = random.NextDouble(); randomDoubles[i] = value; } Этот код очень похож на тот, ко- торый ис- пользовался в классе CardPicker. Возьми в руку карандаш Решение
дальше 4 173 ориентируемся на объекты Экземпляры хранят данные в полях Вывидели, что классы могут содержать какполя, такиметоды. Также вывидели, какключевое словоstaticиспользуетсядляобъявленияполяв классеCardPicker: static Random random = new Random(); Что произойдет, если убрать ключевое слово static? Поле становится полем экземпляра, и каждыйраз,когда высоздаете экземпляр класса, новый экземпляр получает собственную копию этого поля. Чтобывключить поля вдиаграммукласса, разделите прямоугольниккласса гори- зонтальнойлинией.Надлиниейперечисляютсяполя,а методы перечисляются по д линией. Cl ass Field1 Field2 Field3 Met hod1 Met hod2 Met hod3 Методы определяют, что делает объект. Поля содержат информацию, известную объекту. Когда прототипАннысоздал триэкземпляра классаEnemy,каждыйиз этихобъектовисполь- зовался для отслеживания отдельного врага в игре. В каждом экземпляре хранитсяиндивиду- альная копияоднихи техжеданных: изменениеполя экземпляра enemy2никакневлияетна экземпляры enemy1 или enemy3. Поведение объекта определяется его методами, а поля используются для отслеживания его состояния. En emy LastLocationSpotted SearchForPlayer SpottedPlayer C ommunic at eP lay erLoc at io n StopSearching C apt ureP lay er Здесь надиаграмме кл ассов п еречи сл я- ются поля. Каждый экземпляр класса со- держит собственную копию всех полей, в которых хранится ег о сост оян ие. Каждый враг в игре Анны ис- пользует поле дляхранения ин- формации о том, где он в послед- ний раз заметил игрока. На диаграммах классов обычно перечисляются все поля и методы класса. Они называются компонентами класса. L evel E nemi es ResetEnemies Помните, как в классе Level массив исполь- зовался для хранения объек- тов Enemy? Это было поле!
174 глава 3 статические компоненты как единая общая копия Да! Именнодля этого в объявлении было использовано ключевое слово static. Взглянитеещераз на начальные строки классаCardPicker: class CardPicker { static Random random = new Random(); public static string PickSomeCards(int numberOfCards) Есливыуказали ключевое слово static при объявлении поля или метода в классе, то для обращения кэтомуполю илиметодуне обязательноуказывать экземпляр класса.Вы просто вызываетеметод с указанием именикласса: CardPicker.PickSomeCards(numberOfCards) Так вызываются статические методы. Если же в объявлении класса PickSomeCards от- сутствует ключевое слово static, то для вызова метода придется создать экземпляр CardPicker. Если не считать этогоразличия, статические методы ведут себя точно так же, как методы объектов: они могут получать аргументы, могут возвращать значения исуществуютвнутри классов. Я использовал ключевое словоnew для создания экземпляра Random, но нигде не создавал новый экземпляр класса CardPicker. Означает ли это, что методы могут вызываться без создания объектов? В:Когда что-то называют «статическим», обычно име- ется в виду, что оно остается неизменным. Означает ли это, что нестатические методы могут изменяться, а ста - тические методы — нет? Они ведутсебя по-разному? О: Нет, статические методы ведут себя абсолютно одинаково. Единственноеразличие заключается в том, что статические методы не связаны с конкретным экзем- пляром, а нестатические — связаны . В: Значит, я не смогу использовать класс, пока не создам экземпляр объекта? О: Высможетеиспользовать егостатическиеметоды, но есликласс содержит нестатическиеметоды, то перед их использованием придется создать экземпляр. В: Зачем нужны методы, которым необходим эк- земпляр? Почему бы не объявить все методы ста- тическими? О:Потому,чтоесли ваш объект отслеживает некоторые данные(как экземпляры классаEnemy,в которых храни- лись сведения оразных врагах в игре), методы каждого экземпляра могут использоваться для работы с этими данными. Таким образом, когда игра Анны вызывает метод StopSearching для экземпляра enemy2, только этот враг перестает искать игрока. Вызов метода никак не влияет на объекты enemy1 иenemy3, они продолжа- ют поиски. Так Анна может создавать прототипы игры с любым количеством врагов, и ее программы смогут отслеживать всех этих врагов одновременно. Если поле объявлено статическим, его единст­ венная копия совместно использу­ ется всеми экземп­ лярами. Устатическогополясуществует только одна копия,которая совместно используется всемиэкземпля- рами. Следовательно,есливы создадитенесколькоэкземпляровCardPicker,всеонибудут использовать одно итоже поле random. Статическимдаже можнообъявить целыйкласс;тогдавсе поляиметодыэтого класса должны быть статическими. Есливы попытаетесь добавить нестатическийметодв статический класс,то ваша программа не построится. Часто задаваемые вопросы
дальше 4 175 ориентируемся на объекты Перед вами консольное приложение .NET, которое выводит несколько строкна консоль.Приложениевключает классClownсдвумяполями,Name и Height, а также метод с именем TalkAboutYourself. Вам предлагается про- читать код изаписатьстроки,которыебудут выведенына консоль. Диаграммаклассаикод класса Clown: Cl own N ame H eight Tal kA bout Your sel f class Clown { public string Name; public int Height; public void TalkAboutYourself() { Console.WriteLine("My name is " + Name + " and I'm " + Height + " inches tall."); } } Далееследует метод Mainконсольногоприложения.Рядом скаждым вызовом метода TalkAboutYourself,который выводит строку на консоль, располагается комментарий. Заполните пропуски в комментариях, чтобы они соот- ветствоваливыводимым данным. static void Main(string[] args) { Clown oneClown = new Clown(); oneClown.Name = "Boffo"; oneClown.Height = 14; one Clow n.T alk Abou tYo urs elf( ); // My name is _______ and I'm ____ inches tall." Clown anotherClown = new Clown(); anotherClown.Name = "Biff"; anotherClown.Height = 16; anotherClown.TalkAboutYourself(); // My name is _______ and I'm ___ _ inches tall." Clown clown3 = new Clown(); clown3.Name = anotherClown.Name; clown3.Height = oneClown.Height - 3; clo wn3. Tal kAb outY our sel f(); // My name is _______ and I'm ____ inches tall." anotherClown.Height *= 2; anotherClown.TalkAboutYourself(); // My name is _______ and I'm ___ _ inches tall." } Оператор *= приказывает C# взять операнд, ко- торый стоит слева от оператора, и умножить его на операнд в правой части, после чего эта команда обновляет поле Height. Возьми в руку карандаш
176 глава 3 всё в кучу Ниже приведены результаты, которые будут выводиться программойна консоль. Выделите несколько минут на создание нового консольного приложения .NET, добавьте класс Clown, определите в нем метод Main ивыполните егов отладчике в пошаговом режиме. static void Main(string[] args) { Clown oneClown = new Clown(); oneClown.Name = "Boffo"; oneClown.Height = 14; on eCl own .Tal kAb outY our sel f(); // My name is _______ and I'm ____ inches tall." Clown anotherClown = new Clown(); anotherClown.Name = "Biff"; anotherClown.Height = 16; anotherClown.TalkAboutYourself(); // My name is _______ and I'm __ __ inches tall. " Clown clown3 = new Clown(); clown3.Name = anotherClown.Name; clown3.Height = oneClown.Height - 3; cl own 3.T alkA bou tYou rse lf( ); // My name is _______ and I'm ____ inches tall." anotherClown.Height *= 2; anotherClown.TalkAboutYourself(); // My name is _______ and I'm __ __ inches tall. " } Куча Когда программа создает объект, он существует в части компьютернойпамяти,котораяназываетсякучей.Когда ваш код создает объект командой new, C# немедленно резервируетв куче память, где будут храниться данные этого объекта. Память при запуске приложения. Как видите, она пуста. Boffo Biff Biff Biff 14 16 11 32 Когда программа создает новый объект, он добавляется в кучу. При выполнении этого метода в от- ладчике в пошаговом режиме значение поля Height должно стать равным 14 после выполнения этой строки. Эта строка использует поле Height старого экземпляра oneClown для за- полнения поля Height нового экземпляра clown3. Возьми в руку карандаш Решение
дальше 4 177 ориентируемся на объекты О б ъ е кт C l o w n No 3 “Biff” 11 О б ъ е кт C l o w n No 2 “B iff” 32 О б ъ е кт C l o w n No 1 “ Boffo” 14 О б ъ е кт C l o w n No 3 “Biff” 11 О б ъ е кт C l o w n No 2 “Biff” 16 О б ъ е кт C l o w n No 1 “ Boffo” 14 О б ъ е кт C l o w n No 2 “ Biff” 16 О б ъ е кт C l o w n No 1 “ Boffo” 14 О б ъ е к т C l o w n No 1 // Эти команды создают экземпляр класса Clown // и присваивают значения его полям Clown oneClown = new Clown(); oneClown.Name = "Boffo"; oneClown.Height = 14; oneClown.TalkAboutYourself(); // Эти команды создают второй объект Clown // и заполняют его данными Clown anotherClown = new Clown(); anotherClown.Name = "Biff"; anotherClown.Height = 16; anotherClown.TalkAboutYourself(); // При создании третьего объекта Clown // мы используем данные двух других экземпляров // для присваивания значений его полям Clown clown3 = new Clown(); clown3.Name = anotherClown.Name; clown3.Height = oneClown.Height - 3; clown3.TalkAboutYourself(); // Обратите внимание на отсутствие команды new // Мы не создаем новый объект, а изменяем // объект, уже существующий в памяти anotherClown.Height *= 2; anotherClown.TalkAboutYourself(); Что на уме у вашей программы Присмотримся повнимательнее кпрограммеизупражнения«Возь- мив руку карандаш», начиная с первой строки метода Main. В дей- ствительностиэто две команды,объединенныев одну: Clown oneClown = new Clown(); Объект является экземпляром класса Clown. “ Boffo” 14 А теперь посмотрим, как выглядит куча после вы- полнения каждой группы команд: Эта команда объявляет пере- менную с именем oneClown типа Clown. Эта команда созда- ет новый объект и присваивает его пе- ременной oneClown.
178 глава 3 как сделать методы понятными Иногда код плохо читается Даже если вы неосознаете этого, вы постоянно принимаетерешения относительно структуры вашего кода. Вы используете один метод для выполнения некоторой операции? Разбиваете его на несколько методов?А нужен ливообщеновыйметод?Решения, принимаемыевамиотносительно методов, делают ваш кодболее нагляднымидоступнымили, есливыдействуете неосторожно, намногоболее запутанным. Нижеприведенкомпактный блоккода из программы, управляющей машиной для изготовления шоко- ладных батончиков: int t = m.chkTemp(); if(t>160){ Ttb=newT(); tb.clsTrpV(2); ics.Fill(); ics.Vent(); m.airsyschk(); } Слишком компактный код может создать проблемы Присмотритесь к этому коду.Сможете ли выразобраться,что онделает? Не огорчайтесь,если вам это неудастся— оночень плохо читается! Это объясняетсянесколькими причинами: Ì В программе используются переменные с именами tb,ics и m. Имена выбраныпросто ужасно! Мыпонятиянеимеем, что они делают. И для чего нуженкласс T? Ì МетодchkTemp возвращает целоечисло... ночто онделает?По имениможно предположить,что онпроверяет температуру... чего? Ì Метод clsTrpVполучает один параметр.Что должен содержать этот параметр?Почемуонравен2? Идля чего нужно число160? Код C# в промышленном оборудовании?! Разве C# не предназначен для настольных приложений, бизнес-систем, веб-сайтов и игр? C# и .NET применяются везде... буквально везде. Вы когда-нибудь развлекались с Raspberry PI? Это недорогой одноплат- ный компьютер, и такие компьютеры можно найти на самых разных устройствах. Благодаря концепции Windows IoT («Интернет вещей») ваш код C# может выполняться на таких компьютерах. Существуетбес- платная версия для построения прототипов, так что вы можетеначать эксперименты с оборудованием в любой момент. Оприложениях.NET IoT можнобольшеузнать по адресу https://dotnet.microsoft.com/apps/iot.
дальше 4 179 ориентируемся на объекты Как определить, что должен делать ваш код? Весь код пишется по какой-то причине. А значит, вам придется определить эту при- чину самостоятельно! В данном случае нам повезло — можно обра- титься к руководству, по которому работал программист. Руководства к программам обычно не прилагаются Такиекомандыобычно недают никакихподсказокотносительнотого, почемукодделает то, чтоонделает. Вданном случае программистбылдоволен результатом, потомучто емуудалосьупаковатьвесь кодводин метод. Но компактность кода сама посебеособойпользыне приносит!Разобьем его на методы, чтобы код лучше читался,и позаботимся о том,чтобы имена, присвоенныеклассам, были содержательными. Дляначаларазберемся,что долженделать код. К счастью,нам известно,что этоткод является частью встроеннойсистемы, или контроллера,являющегосячастью большей электрической или механической системы. И еще нам повезло, что мы располагаем документацией по этому коду, а конкретно руковод- ством,котороеиспользовалось программистамипри исходном построении системы. Руководство для машины по изготовлению шоколадных батончиков GeneralElectronics Type 5 Температура нуги должна проверяться каждые 3 минуты автомати- зированной системой.Если температура превышает 160°C , начинка слишком горячая, поэтому система должна выполнить процедуру вентиляции изолированной системы охлаждения (CICS): • Закрыть клапан регулятора на турбине No 2. • Заполнить изолированную систему охлаждения сплошным по- током воды. • Спустить воду. • Запустить автоматизированную проверку присутствия воздуха всистеме. Сравнимкод с руководством,в котором говорится, что долженделать код. Добавление комментариев определенно поможетразобратьсявпроисходящем: /* Этот код выполняется каждые 3 минуты для проверки температуры * Если температура превышает 160 °C, необходимо запустить систему охлаждения. */ int t = m.chkTemp(); if(t>160){ // Получить систему управления для турбин Ttb=newT(); // Закрыть клапан регулятора на турбине 2 tb.clsTrpV(2); // Заполнить и включить вентиляцию изолированной // системы охлаждения ics.Fill(); ics.Vent(); // Запустить проверку воздуха в системе m.airsyschk(); } Комментарии в коде — хо- рошее н ач ало. А в ы сможете пред ложить еще к ак ие- н ибудь решен ия , к оторые сделаю т этот код еще более понятным? Добавление пустых строк в неко- торых местах упрощает чтение вашего кода. Мозговой штурм
180 глава 3 удобочитаемый код упрощает программирование Использование содержательных имен классов и методов Страница из руководства значительно упростила понимание кода. Крометого, она содержитряд по- лезных подсказок, которыепозволяютразобратьсявлогикекода. Посмотримна первые двестроки: /* Этот код выполняется каждые 3 минуты для проверки температуры. * Если температура превышает 160C, необходимо запустить систему охлаждения. */ int t = m.chkTemp(); if(t>160){ Добавленный комментарий многое объясняет. Теперь мы знаем,почему условная команда сравнивает переменную t с 160°, — в руководстве говорится, что при любой температуре, превышающей 160°C, нугастановитсяслишком горячей.Как выясняется,m — класс,управляющиймашинойдляпроизводства шоколадных батончиков; онсодержит статическиеметодыдляпроверкитемпературынугиипроверки системыохлаждения. Давайтевыделимпроверкутемпературывотдельный метод ивыберемдляклассаи методовимена,которые наглядно поясняют ихпредназначение. Первые две строкибудут выделены вотдельныйметод, который возвращаетлогическое значение:true, если нуга слишкомгорячая,или falseпри нормальной температуре: /// <summary> /// Если температура нуги превышает 160 °C, она слишком горячая. /// </summary> public bool IsNougatTooHot() { int temp = CandyBarMaker.CheckNougatTemperature(); if (temp > 160) { return true; }else{ return false; } } Авы заметилиспециальные комментарии///над методом? Они называютсядокументирующими ком- ментариямиXML.IDE использует этикомментарии для вывода справки по методам вродетой,которая выводилась, когда вы использовалиокно IntelliSense для выбора метода класса Random. После того как клас- су будет присвоено имя «CandyBarMaker», а методу — имя «CheckNougatTemperature», код становится более по- нятным. Visual Studio помогает добавлять документацию XML. Установите курсор в строке над любым методом, введите три символа/, и IDEдобавит пустой шаблондлядокументации. Если вашметод имеет параметрыи возвращаемый тип, для нихтакже будутдобавлены теги <param> и <returns>.Попробуйте вернутьсякклассуCardPicker и ввести ///встроке над методом PickSomeCards —IDE добавит пустой шаблон документации XML. Заполните его и убедитесь в том, что информацияпоявляетсяв IntelliSense. /// <summary> /// Выбирает несколько карт и возвращает их /// </summary> /// <param name="numberOfCards">Количество выбираемых карт.</param> /// <returns>Массив строк с названиями карт.</returns> Документацию XML также можно создавать и для полей. Попробуйте установить курсор в строке над любым полем и ввести три символа/вIDE.Весьтекст, который следуетза <summary>, выводитсяв окнеIntelliSenseдляэтого поля. Об использовании IDE: документация XML для методов и полей А вы заметили, что буква C в имени CandyBarMaker имеет верхний ре- гистр? Если имена классов всегда начинаются с буквы верхнего регистра, а имена переменных — с букв нижнего регистра, вам будет проще по- нять, когда вызывается статический метод, а когда вызывается метод экземпляра.
дальше 4 181 ориентируемся на объекты Чтопредлагает делать руководство, если нуга слишком горячая?Оно предлагает выпол- нить процедурувентиляцииизолированнойсистемыохлаждения (CICS).Создадим другой метод, выберем содержательное имя для класса T(который,как выяснилось, управляет турбиной)и длякласса ics (которыйуправляет системойохлаждения и содержитдва ста- тических метода длязаполненияи вентиляциисистемы),а заодно дополним все краткой документацией XML: /// <summary> /// Выполнить процедуру вентиляции изолированной системы охлаждения (CICS). /// </summary> public void DoCICSVentProcedure() { TurbineController turbines = new TurbineController(); turbines.CloseTripValve(2); IsolationCoolingSystem.Fill(); IsolationCoolingSystem.Vent(); Maker.CheckAirSystem(); } Итак,теперь унас имеются методы IsNougatTooHot иDoCICSVentProcedureи мыможем переписать исходныйневразумительныйкодв виденовогометодаиприсвоить емуимя,которое четко выражает, что он делает: Когда ваш метод объявляется с возвращаемым типом void, это означает, что он не возвращает зна- чения и команда return ему не нуж- на. Во всех методах, написанных вами в предыдущей главе, исполь - зовалось ключевое слово void! Temper at ur eCh ecker Thr eeMinut eChec k DoCI CS V ent P roc edure I sN ougat TooHot Используйте диаграммы классов для планирования Диаграмма класса — полезный инструмент для проектиро- вания вашего кода ДО ТОГО, как вы начнете писать этот код. Запишите имя класса в верхней части диаграммы. Затем запишите все методы в прямоугольнике в нижней части. Теперь все части класса видны с первого взгляда, а у вас по- является возможность обнаружить проблемы, которые за- труднят использование вашего кода или его понимание. Мы упакова- ли новые методы в класс с именем TemperatureChecker. Диаграмма класса выглядит так: /// <summary> /// Этот код выполняется каждые 3 минуты для проверки температуры. /// Если температура превышает 160 °C, необходимо запустить систему охлаждения. /// </summary> public void ThreeMinuteCheck() { if (IsNougatTooHot() == true) { DoCICSVentProcedure(); } } Код сталнамного более понятным!Дажеесливынезнаете,что процедура вентиляции CICS должна выполняться, если нуга слишком горячая, стало намного понятнее, что делает этот код.
182 глава 3 постоянный рефакторинг Все верно. Когда вы изменяете структуру кода без изменения его поведения, этот процесс называется рефакторингом. Опытные разработчики пишут предельно простой и понятный код (втомчислеивам, если вы не прикасались к нему в течение долгого времени).Комментарии полезны, но ничто не сравнится с выбором осмысленных имендляметодов,классов, переменных иполей. Чтобыупростить чтение инаписание кода, вам следуетосновательно задуматься над задачей, для которой был написан ваш код. Если вы- брать имена методов, которые будут понятны каждому, кто понимает вашузадачу,то вашкодбудет намного прощекакдляанализа,такидля разработки. Как бы мы ни планировали свой код, почти никогда не удаетсясделать все правильно с первогораза. Вотпочемуопытныеразработчикипостоянно проводятрефакторингсвоего кода.Ониперемещают свойкодвметодыиприсваивают этимметодам осмысленныеимена.Онипереименовывают переменные. Каждыйраз, когдаониобнаруживаюткод, неочевидныйна сто процентов, онитра- тятнесколько минутна егорефакторинг. Онизнают,чтоэто времяне пропадет даром, потомучтоэтапроцедураупростит добавление нового кода черезчас(через день,месяц или год!). Постойте, сейчас мы сделали нечто действительно интересное! Мы только что внесли множество изменений в блок кода. Теперь он выглядит совершенно иначе и читается намного проще, но при этом делает то же самое.
дальше 4 183 ориентируемся на объекты Del iver yGuy AddAPizza PizzaDelivered TotalCash Ret urnT ime В каждом из этих классов имеется серьезный структурный не- достаток. Напишите, что, по вашему мнению, не так с каждым классом и какбывырешилипроблему. C lass23 Candy B arW eight PrintWrapper G enerat eRepor t Go Эти два класса являются частью системы, которая используется пиццерией для отслеживания заказов, переданных в доставку. Этот класс является частью системы производства шоколадных батончиков, описанной выше. Cash Reg ist er Mak eS ale NoSale P umpG as Refund Tot alC ashI nR egis t er GetTransactionList A ddCas h R emoveC ash Класс CashRegister является частью программы, используемой системойоплатыв круглосуточном магазине при автозаправке. D eli veryGi rl AddAPizza PizzaDelivered Tot alC ash Ret ur nTi me Возьми в руку карандаш
184 глава 3 несколько полезных советов Ниже показаныпредложенные намиисправления. Мы показываем лишь один извозможных способов решения проблемы, тем не менее,существует многодругихспособовисправленияархитектурыэтихклассоввзависимости от их использования. C and yMaker Candy Bar Wei ght PrintWrapper Gener at eReport MakeT heCandy Эти два класса являются частью системы, которая используется пиццерией для отслеживания заказов, переданных в доставку. Этот класс является частью системы производства шоколадных батончиков, описанной выше. Класс CashRegister является частью программы, используемой системойоплаты вкруглосуточном магазине при автозаправке. D eli veryPer son Gender AddAPizza PizzaDelivered TotalCash Ret urnTi me Имя класса не описывает назначение класса. Программист, ко- торый видит строку с вызовом Class23.Go, понятия не имеет, что делает эта строка. Также методу запуска системы при- своено более содержательное имя — мы выбрали MakeTheCandy, но оно вполне может быть другим. Похоже, классы DeliveryGuy и DeliveryGirl делают одно и то же — они хранят информацию о курьере, который выехал на доставку пиццы. Правильнеебыло бы заменить их одним клас- сом, в который добавляется поледля хранения пола курьера. Все методы класса связаны с кассовыми операциями — про- дажей, получением списка операций, добавлением наличных... кроме одной: заправки бензином (PumpGas). Этот метод лучше вынести из класса и включить его вдругой класс. Мы решили НЕ добавлять поле Gender, потому что на самом деле пиццерии совершенно незачем хранить данные о поле курьера — следует уважать их личную инф орм а цию! Cash Reg ist er MakeS al e NoS ale Ref und Tot al Cas hI nRegis t er GetTransactionList AddC ash Remov eCash Возьми в руку карандаш Решение
дальше 4 185 ориентируемся на объекты Есливызашли втупик при написании кода, не огорчайтесь. На самом деле это даже хорошо! Сутьюнаписания кода является решение задач, а некоторые задачи оказываются очень хитрыми!Но если помнить некоторые обстоятельства, программирование пойдет намного эф ф ективнее: Ì Программисту очень легко столкнуться с синтаксическими проблемами (такими, как про- пущенные круглые скобки или кавычки). Одна пропущенная скобка может породить сразу несколько ошибокпри построении. Ì Гораздо лучшесмотреть нарешение, чеммучитьсянадзадачей.Когда выиспытываете досаду, мозг нежелаетучиться. Ì Весь код в книге был протестирован, и он определенно работает в Visual Studio2019!Тем не менее при вводе легко допустить ошибку(например, ввести 1 вместо буквыL в нижнем р егис тр е). Ì Есливашерешениенестроится,попробуйте загрузитьегоизрепозиторияGitHub этойкни- ги— внем содержитсяработоспособныйкод для всех упражнений вкниге:https://gitgub.com/ head-first-csharp/fourth-edition. При чтении кода можно получить много полезной информации. Таким образом, если вы столкнулись с проблемой при выполнении упражнений, не бойтесь подсматривать в реше- ние. Это не жульничество! Мы собираемся вернуться к написанию кода. Немало кода будет написано в оставшейся части этой главы и МНОГО кода в книге. Это означает, что мы будем создавать множество классов. Приведем несколько советов, о которых стоит помнить, когда вы принимаетерешения по их проектированию: Ì Ваша программа строится для решения определенной задачи. Выделите время на обдумывание задачи.Насколько легко она разбивается на части?Как бы вы объяснили эту задачу другим?Все это стоит продумать при проектировании классов. Ì Какие реальные сущности будут использоваться в программе? В программе для отслеживания графиков кормления животных в зоопарке могут присутство- вать классы для разных видов корма и типов животных. Ì Используйте содержательные именадля классов и методов. Чтобы другиелюди могли получить представление о том, что делают ваши классы и методы, им должнобыть достаточно просто взглянуть на их имена. Ì Ищите сходство между классами. Иногда два класса можно объединить в один, если эти классы действительно похожи. Система производства шоколадных батончиков может содержать три или четыре турбины, но только один метод для закрытия клапана регулятора, который получает номер турбины в параметре. Несколько идей для проектирования понятных классов РЕ Л А К С
186 глава 3 рабочий класс Классы, парни и деньги ДжоиБобпостоянноодалживают другдругуденьги.Создадим классдляхраненияинформации отом, сколькоденег имеетсяу каждого изних. Начнемс краткого обзора того, чтонам предстоит сделать. О б ъ е к т G u y No 2 О б ъ е кт G u y No 1 Guy Name Cas h WriteMyInfo GiveCash Rec ei veCas h Мы создадим два экземпляра класса Guy. Дляхранения экземпляров будут использоваться две переменные Guyс именамиjoeиbob.Послеихсоздания кучабудетвыглядеть так: 1 Мы присвоим значения полей Cash и Name каждого объекта Guy. Дваобъекта представляютдвухразныхпарней сразными именами иразными суммамиденегвкармане.У каждого парня имеетсяполеNameдляхранения имени,а такжеполе Cash для хранения суммы денег. 2 О б ъ е к т G u y No 2 “ Bob” 50 О б ъ е к т G u y No 1 “ Joe” 100 Мы добавим методы для передачи и получения денег. Привызовеметода GiveCash парень выдает деньги (а значение его поляCash уменьшается); методвозвращаетвыданную сумму.Чтобы парень получилденьги иположил ихвкарман(увеличилсвоеполеCash), вызываетсяметодReceiveCash, которыйвозвращает полученную сумму. 3 О б ъ е к т G u y No 2 “ Bob” 75 Метод ReceiveCash добавляет деньги в карман Боба, увеличивая поле Cash объекта на указан- ную сумму (так что у Боба будет 75 долла- ров), после чего возвращает добавленную сумму. Чтобы дать Бобу 25 долларов, мы вызываем его ме- тод ReceiveCash (потому что он получает деньги). О б ъ е к т G u y No 2 “ Bob” 50 bob .Re ceiv eCa sh( 25); Мы выбрали осмысленные имена методов. Чтобы получить от объекта Guy некоторую сумму, следует вызвать его метод GiveCash, а чтобы дать емуденег — вызвать метод ReceiveCash.
дальше 4 187 ориентируемся на объекты class Guy { public string Name; public int Cash; /// <summary> /// Выводит значения моих полей Name и Cash на консоль. /// </summary> public void WriteMyInfo() { Console.WriteLine(Name + " has " + Cash + " bucks."); } /// <summary> /// Выдает часть моих денег, удаляя их из кармана (или выводит на консоль /// сообщение о том, что денег недостаточно). /// </summary> /// <param name="amount">Выдаваемая сумма.</param> /// <returns> /// Сумма денег, взятая из кармана, или 0, если денег не хватает /// (или если сумма недействительна). /// </returns> public int GiveCash(int amount) { if (amount <= 0) { Console.WriteLine(Name + " says: " + amount + " isn't a valid amount"); return 0; } if (amount > Cash) { Console.WriteLine(Name + " says: " + "I don't have enough cash to give you " + amount); return 0; } Cash -= am ount; return amount; } /// <summary> /// Получает деньги, добавляя их в мой карман (или выводит /// сообщение на консоль, если сумма недействительна). /// </summary> /// <param name="amount">Получаемая сумма.</param> public void ReceiveCash(int amount) { if (amount <= 0) { Console.WriteLine(Name + " says: " + amount + " isn't an amount I'll take"); } els e { Cash += amount; } } } В полях Name и Cash хранится имя парня и сумма денег у него в кармане. Иногда бывает нужно приказать объекту выполнить некоторую операцию — напри- мер, вывести описа- ние этого объекта на конс оль . Методы GiveCash и ReceiveCash про- веряют, что сумма, которую требуется выдать или полу- чить, действитель- на. Это делается для того, чтобы нельзя было вызвать метод получения денег с от- рицательным значе- нием, что привело бы к потере средств. Сравните комментарии в коде с диаграммой класса и схемами объектов Guy. Если что-то покажется непонятным, не жалейте времени и основательно разберитесь в происходящем.
188 глава 3 начинаем с создания экземпляров Простой способ инициализации объектов в C# Почтикаждый объект, который высоздаетев программе,необходимокаким- тообразоминициализировать. ОбъектGuy неявляетсяисключением — он бесполезен, пока вы не зададите его поля Name и Cash. Необходимость винициализацииполейвстречается так часто,что вC# длянеесуществует специальнаясокращеннаязапись, называемаяинициализаторомобъектов. Функция IDE IntelliSense поможетвам в работес ней. Сейчас мыопробуемее при создании двухобъектовGuy.Конечно, выможете использовать команду newиещедвекомандыдляинициализацииееполей: joe = new Guy(); joe.Name = "Joe"; joe.Cash = 50; Вместо этого введите команду Guy joe = new Guy() { Как только вы введете левую фигурную скобку, IDE открывает окно IntelliSenseсоспискомвсехполей,которыевы можетеинициализировать: ВыберитеполеCash,присвойтезначение50 идобавьтезапятую: Guyjoe=newGuy(){Cash=50, Теперь введите пробел — открываетсяеще одно окно IntelliSense с оставшимсяполем: ЗадайтезначениеполяNameивведите точкус запятой.Теперьу васесть одна команда, которая инициализирует ваш объект: Guyjoe=newGuy(){Cash=50,Name="Joe"}; Инициализаторы объектов экономят время, делают ваш код более компактным и удобочитаемым... а IDE помогает вам записывать их. Новое объявление делает то же самое, что и три строки в начале страни- цы, но оно короче и проще читается. Теперь у вас есть все необходимое для построения кон- сольного приложения, использующего два экземпляра класса Guy. Его результаты выглядят примерно так: Сначала оно вызывает методWriteMyInfo каждого объекта Guy. Затем приложение читает сумму из входного потока и спрашивает, кто должен получить эту сумму. После это- го оно сначала вызывает метод GiveCash одного объекта Guy, а затем метод ReceiveCash другого объекта Guy. Это продолжается до тех пор, пока пользователь не введет пу- стую строку.
дальше 4 189 ориентируемся на объекты Ниже приведен метод Main для консольного приложения, которое обеспечивает передачу денег между объектами Guy. Вам предлагается заменить комментарии кодом — прочитайте каждый комментарий и напишите код, который выполняет описанные операции. После этого у вас появится программа, которая работает так, как показано на снимке экрана на преды- дущей странице. static void Main(string[] args) { // Создайте новый объект Guy в переменной с именем joe // Присвойте его полю Name значение "Joe" // Присвойте его полю Cash значение 50 // Создайте новый объект Guy в переменной с именем bob // Присвойте его полю Name значение "Bob" // Присвойте его полю Cash значение 100 while (true) { // Вызовите методы WriteMyInfo для каждого объекта Guy Console.Write("Enter an amount: "); string howMuch = Console.ReadLine(); if (howMuch == "") return; // Используйте метод int.TryParse для преобразования строки howMuch в int // (как это было сделано ранее в этой главе) { Console.Write("Who should give the cash: "); string whichGuy = Console.ReadLine(); if (whichGuy == "Joe") { // Вызовите метод GiveCash объекта joe и сохраните результат // Вызовите метод ReceiveCash объекта bob с сохраненным результатом } else if (whichGuy == "Bob") { // Вызовите метод GiveCash объекта bob и сохраните результат // Вызовите метод GiveCash объекта joe с сохраненным результатом } e lse { Console.WriteLine("Please enter 'Joe' or 'Bob'"); } } els e { Console.WriteLine("Please enter an amount (or a blank line to exit)." ); } } } Замените все ком- ментарии кодом, который выполняет описанные операции. Упражнение
190 глава 3 погодите, это еще не все Ниже приведен метод Main для консольного приложения. В нем используется бесконечный цикл, который запрашивает у пользователя, сколько денег следует передать между объектами Guy. Если пользователь вводит пустую строку, метод выполняет команду return, что приводит к выходу из метода Main и завершению программы. static void Main(string[] args) { Guyjoe =new Guy(){Cash =50,Name ="Joe" }; Guy bob = new Guy() { Cash = 100, Name = "Bob" }; while (true) { joe.WriteMyInfo(); bob.WriteMyInfo(); Console.Write("Enter an amount: "); string howMuch = Console.ReadLine(); if (howMuch == "") return; if (int.TryParse(howMuch, out int amount)) { Console.Write("Who should give the cash: "); string whichGuy = Console.ReadLine(); if (whichGuy == "Joe") { int cashGiven = joe.GiveCash(amount); bob.ReceiveCash(cashGiven); } else if (whichGuy == "Bob") { int cashGiven = bob.GiveCash(amount); joe.ReceiveCash(cashGiven); } els e { Console.WriteLine("Please enter 'Joe' or 'Bob'"); } } el se { Console.WriteLine("Please enter an amount (or a blank line to exit). "); } } } В этом коде один объект Guy от- дает деньги из своего кармана, а другой объект Guy получает их. Когда метод Main выполняет эту команду return, программа завер- шается, потому что консольное приложение прекращает работу при завершении метода Main. Не переходите к следующей части упражнения, пока первая часть не заработает и вы не пойме- те, что в ней происходит. Выделите несколько минут на то, чтобы выполнить программу в по- шаговом режиме отладчика, и убедитесь в том, что вы ее действительно поняли. Упражнение Решение
дальше 4 191 ориентируемся на объекты Итак, у вас имеется работающий класс Guy. Попробуем использовать его в азартной игре. Присмотритесь к следующему снимку экрана, попробуйте понять, как работает программа и что она выводит на консоль. Создайте новое консольное приложение и добавьте тот же класс Guy. Затем в методе Main объявите три пере- менные: переменная Random с именем random инициализируется новым экземпляром класса Random; перемен- ная double с именем oddsдля хранения порога вероятности инициализируется значением 0.75; переменная Guy с именем player инициализируется экземпляром Guy с именем "The player" и значением 100. Выведите на консоль строку с приветствием и порогом вероятности. Затем запустите следующий цикл: 1. Объект Guy выводит сумму денег в своем кармане. 2. Запросите у пользователя размер ставки. 3. Прочитайте строку в строковую переменную с именем howMuch. 4. Попытайтесь преобразовать ее в переменную int с именем amount. 5. Если попытка преобразования завершится удачно, ставка игрока присваивается переменной int с име- нем pot. Она умножается на 2, потому что в результате игры игрок либо получает удвоенную ставку, либо теряет свою ставку. 6. Программа выбирает случайное число от 0 до 1. 7. Если число больше odds, игрок получает сумму из pot. 8. В противном случае игрок теряет ставку. 9. Программа продолжает работать, пока у пользователя остаются деньги. Дополнительный вопрос: имя Guy действительно хорошо подходит для класса? По- чему да (или почему нет)? Порог вероятности Во время каждыого хода игрок делает ставку. При выигрыше он получает удвоенную ставку, а при проигрыше ставка теряется. Программа выбирает случайное число от 0до 1. Если число больше порога вероятности, игрок полу- чает удв оен ную ст авк у; в противном случае ставка т еряет ся. (часть 2) Упражнение Возьмив руку карандаш
192 глава 3 усваиваем работу с экземплярами Ниже приведен рабочий метод Main для игры. Сможете ли вы предложить, как сделать его более интересным? Удастся ли вам придумать, как добавить новых игроков, как поддерживать разные варианты порога вероятности, а может , вы предложите что-то , еще более занятное? Это ваша возможность проявить фантазию! static void Main(string[] args) { double odds = .7 5; Random random = new Random(); Guy player = new Guy() { Cash = 100, Name = "The player" }; Console.WriteLine("Welcome to the casino. The odds are " + odds); while (player.Cash > 0) { player.WriteMyInfo(); Console.Write("How much do you want to bet: "); string howMuch = Console.ReadLine(); if (int.TryParse(howMuch, out int amount)) { int pot = player.GiveCash(amount) * 2; if(pot>0) { if (random.NextDouble() > odds) { int winnings = pot; Console.WriteLine("You win " + winnings); player.ReceiveCash(winnings); } else { Console.WriteLine("Bad luck, you lose. "); } } } else { Console.WriteLine("Please enter a valid number." ); } } Console.WriteLine("The house always wins. "); } Наш ответ на дополнительный вопрос — а ваш ответ был другим? Когда мы использовали объекты Guy («парень») для представления Джо и Боба, имя было подходящим. Но теперь, когда оно используется для представления игрока, лучше выбрать более содержательное имя класса (такое, как Bettor или Player). Ваш код слегка отличается от нашего? Если он работает и выдает правильные результаты, это нормально! Одну и ту же програ мм у можно написать многими разными способами. . ..а так же не м ного потренироваться в напи- сании кода — как говорилось выше, это луч- ший способ стать хорошим разработчиком. Упражнение Решение Возьмивруку карандаш .. .и по мере того как вы будете про- двигаться в изуче- нии книги, а ответы к упражнениям будут становиться все длин- нее, ваш код будет все сильнее отличаться от нашего. Помните: всегда можно под- смотреть в решение, пока вы работаете над упражнением!
дальше 4 193 ориентируемся на объекты Перед вамиконсольное приложение .NET, которое выводит тристроки на кон- соль. Постарайтесь определить, какие результаты оно выведет, без использо- вания компьютера. Начните с первойстроки метода и отслеживайте значения всехполейобъектав процессевыполнения. class Pizzazz { public int Zippo; public void Bamboo(int eek) { Zippo += eek; } } class Abracadabra { public int Vavavoom; public bool Lala(int floq) { if (floq < Vavavoom) { Vavavoom += floq; return true; } return false; } } class Program { public static void Main(string[] args) { Pizzazz foxtrot = new Pizzazz() { Zippo = 2 }; foxtrot.Bamboo(foxtrot.Zippo); Pizzazz november = new Pizzazz() { Zippo = 3 }; Abracadabra tango = new Abracadabra() { Vavavoom = 4 }; while (tango.Lala(november.Zippo)) { november.Zippo *= -1; november.Bamboo(tango.Vavavoom); foxtrot.Bamboo(november.Zippo); tango.Vavavoom - = foxtrot.Zippo; } Console.WriteLine("november.Zippo = " + november.Zippo); Console.WriteLine("foxtrot.Zippo = " + foxtrot.Zippo); Console.WriteLine("tango.Vavavoom = " + tango.Vavavoom); } } Что программа выведет на консоль? november.Zippo = foxtrot.Zippo = tango.Vavavoom = Чтобы проверить решение, введите программу в Visual Studio изапустите ее. Если ответ оказался ошибочным,выполните про- граммув пошаговом режиме ипроследите за изменениямивсех полей объекта. Если вы не хотите вводить весь код вручную, его можно загру- зить по адресу https://gitgub.com/head-first-csharp/fourth-edition. Если вы используете Mac, IDE сге- нерирует класс с именем MainClass вместо Program. В данном упражне- нии это ни на что не повлияет. Возьми в руку карандаш
быстрое выполнение кода ¢ Ключевое слово new используется для создания экземпляров класса. В программе может быть много экземпляров одного класса. ¢ Каждый экземпляр содержит все методы класса и по- лучает собственные копии всех полей. ¢ При включении в код new Random(); создается экзем- пляр классаRandom. ¢ Ключевое слово static объявляет поле или метод класса статическим. Для обращения к статическим ме- тодам или полям не нужноуказывать экземпляр класса. ¢ Если поле является статическим, то существует только одна копия такого поля, которая совместно использует- ся всеми экземплярами. Если ключевое слово static включено в объявление класса, все поля и методы та- кого класса тоже должныбыть статическими. ¢ Если убрать ключевое слово static из объявления статического поля, оно становится полем экземпляра. ¢ Поля и методы класса называются его компонентами. ¢ Когда программа создает объект, он существует в спе- циальной части компьютерной памяти, которая называ- ется кучей. ¢ Visual Studio помогает добавлять документацию XML к полям и методам и выводит ее в окнеIntelliSense. ¢ Диаграммы классов помогают планировать классы и упрощают работу с ними. ¢ Когда вы изменяете структуру кода без изменения его по- ведения, это называется рефакторингом. Опытныераз- работчики постоянно проводятрефакторинг своего кода. ¢ Инициализаторы объектов экономят время, делая ваш код более компактным иудобочитаемым. КЛЮЧЕВЫЕ МОМЕНТЫ Используйте интерактивное окно C# для выполнения кода C# Если вы хотите выполнить фрагмент кода C#, можно обойтись и без создания нового проекта вVisual Studio. Любой код C#, введенный в интерактивном окне C#, выпол- няется немедленно. Окно открывается командой View>>Other Windows>>C#Interactive. Попробуйте открыть егоивставьтекодиз ответа купражнению.Чтобывыполнитьего, введите следующую команду и нажмитеEnter: Program.Main(new string[] {}). Если вы используете Mac,в вашей IDE может не быть интерактивно- го окна, но вы можете выполнить команду csiв Терминале, чтобы воспользоваться интер- активным компилято- ром C# dotnet. В параметре "args" переда- ется пустой массив. Также можно запустить интерактивный сеанс C# из командной строки. В системе Windows проведите в меню Пуск поиск по текстуdeveloper commandprompt, запуститеокно командной строки и введите csi. В macOS или Linux выполните команду csharp, чтобы запустить оболочку Mono C# Shell. В обоих случаяхвы можете вставить классыPizzazz, Abracadabra иProgram из предыдущего упражнения прямо в приглашении, после чего выполните команду Program.Main(new string[] {}) для запуска точки входа вашего консольного приложения. Вста вьте все классы. После каж- дой вставленной строки следует серия точек. Не об- ращайте внимания на ошибку с упомина- нием точ- ки входа. Выполните метод Main, чтобы просмотретьрезультат. На- жмите Ctrl+D, чтобы завер- шить работу.
Типы и ссылки 4 Данные и ссылки Сборщик мусора явился за данными. Чем были бы наши приложения без данных? Задумайтесь на минуту. Без дан- ных наши программы... в общем, трудно представить, что кто -то станет писать код без данных. Вы запрашиваете информацию у ваших пользователей; эта информация используется для поиска данных или генерирования новой информации, которая воз- вращается пользователю. Собственно, практически все, что вы делаете в программи- ровании, требует работы с данными в той или иной форме. В этой главе вы изучите все тонкости типов данных и ссылок C#, поймете, как работать с данными в про- граммах, и даже узнаете кое-что новое об объектах (представьте, объекты — тоже данные!).
196 глава 4 вы д ающ ий ся мастер Оуэну нужна наша помощь! Оуэн — гейм-мастер(иоченьхороший).Онведетгруппу, ко- торая еженедельно собирается у него дома для проведения ролевыхигр (RPG). И как любой хороший гейм-мастер, он основательно трудится, чтобысделатьвремяпрепровождение интересным дляигроков. Повествование, фантазия и механика Оуэн —особеннохорошийрассказчик.За несколько последних лет он создалпродуманныйфантастиче- скиймир для своихдрузей, ноон не совсем доволен механикойигровойсистемы. Поможем ли мы Оуэну улучшить его RPG? Характеристики персонажа (сила, выносливость, харизма, интеллект) стали важной механикой во многих ролевых играх. Игроки часто бро- сают кубики и определяют харак- теристики своих персонажей по специальным формулам.
дальше 4 197 типы и ссылки На листах персонажей хранятся разные виды данных Есливыкогда-нибудь играли вRPG, то вы уже видели листы персо- нажей:страницус подробнойинформацией, статистикой, историей ивообщелюбымидругимизаметками, которыеможно сделать попо- воду персонажа. Есливыхотите создатькласс, представляющийлист персонажа,какиетипы вы бы использовали для его полей? CharacterSheet CharacterName Level PictureFilename Alignment CharacterClass Strength Dexterity Intelligence Wisdom Charisma SpellSavingThrow PoisonSavingThrow MagicWandSavingThrow ArrowSavingThrow ClearSheet GenerateRandomScores CharacterName Ch ar acter Sh eet Level Alignment CharacterClass Picture Str engt h Dexterity Intelligence Wisdom Charisma Spel l Savi ng Throw Poison SavingThrow Magic Wand SavingThrow Arrow Saving Throw ELLIWYNN 7 LAWFUL GOOD WIZARD 911 10 17 15 В RPG, в которую играет Оуэн, спас-броски дают игроку шанс избежать некоторых видов атак, бросая кубики. У этого персонажа имеется спас-бросок от маги- ческого жезла, поэтому игрок закрасил этот кружок. Игроки создают персо- нажей, бросая кубики для каждой из своих харак- теристик, и записывают результаты в этих полях. Поле для портрета персонажа. Если бы вы строили класс C# для представления листа персо- нажа, изображение можно было бы сохранить в графическом файле. Взгляните на поля, перечисленные на диаграмме класса CharacterSheet. Какой тип вы бы выбрали для к аждого пол я? Мозговой штурм
198 глава 4 знай свои типы Лучше остроумный дурак, чем глупый остряк. Тип переменной определяет, какие данные в ней могут храниться ВC#встроеномного типов данных,предназначенныхдля храненияразных видовинформа- ции. Вамужеизвестны такиераспространенныетипы,как int,string,bool иfloat.Такжеесть идругиетипы, которыевам ещене попадались; онитоже могут принестипользу. Некоторые типы, которыми вы будете активно пользоваться. Ì int позволяет хранить любое целое число от –2 147 483648 до 2147483647.Целые числа неимеютдробной части. Ì string можетхранить текстпроизвольной длины (включая пу- стыестроки ""). Ì bool используется для логи- ческихзначений — true или false. Он используетсядля п р едста вл ения всег о, что может находитьсятолько водном издвух состояний: либо одно, либо другое. Ì float позволяет хранить вещес твенные чис ла от ±1.5×10–45до ±3.4 ×1038 с точностью до 8 знаков. Ì double позволяет хранить вещественные числа от ±5.0×10–324до ±1.7×10308 с точно- стью до 16 знаков. Этоттип очень часто ис- пользуется приработесо свойствамиXAML. Как вы думаете, почему в C# предусмотрено несколько типов для хранения чисел сдробной ч аст ью? Мозговой штурм
дальше 4 199 типы и ссылки В C# существует несколько типов для хранения целых чисел ВC#дляхраненияцелыхчиселсуществуютидругиетипы —не толькоint. На первыйвзглядэтовыглядит немного странно. Зачем создавать столько типов для чисел без дробной части?Длябольшинства про- грамм вкнигеневажно,будетеливы использоватьintилиlong.Но есливы пишетепрограмму,которая должна хранить многие миллионы целых значений, выбор меньшего типа (например,byte) вместо большего типа (такого, как long) может сэкономить очень много памяти. Ì В типеbyteможетхраниться любое целоечисло от0 до 255. Ì В типеsbyte можетхранитьсялюбоецелое число от –128до 127. Ì В типеshort может хранитьсялюбоецелое число от–32768до 32767. Ì В типеlongможет храниться любое целоечисло от–9233372036854775808 до 9 233 372 036 854 775 807. Авызаметили,чтов типе byte хранятсятолько положитель- ныечисла,тогда как sbyteтакжеподходитдляотрицатель- ных чисел? Оба типа позволяют хранить 256возможных значений.Различие втом, что значения типа sbyte (как и shortи long)могутбыть отрицательными,вот почему они называютсятипами со знаком («s» в sbyteозначает«sign», т.е. «знак»!).И еслиbyte является версиейsbyte без знака, такие версиисуществуют иутиповshort,intи long;ихимена начинаютсяс «u»: Ì В типе ushort можетхранитьсялюбоеположительное целоечисло от0 до 65 535. Ì В типеuint может хранитьсялюбоеположительное целое число от 0 до 4 294 967 295. Ì В типеlong может хранитьсялюбоеположительное целое число от 0 до 18 446 744 073 709 551 615. Тип byte хранит только малые целые числа от 0 до 255. Если вам нужно хранить большее число, используйте тип short, который способен хранить целые числа от -32 768 до 32 767. Тип long также хранит целые числа, но в нем могут поместиться огромные значения.
200 глава 4 большие числа, малые числа и вообщенечисла Типы для хранения ОГРОМНЫХ и очень маленьких чисел Иногда точностиfloat оказывается недостаточно. Ихотите верьте, хотите нет, но 1038 может оказаться недостаточно большим, а 10 –45 — недостаточно малым. Мно- гиепрограммы,написанныедлянаучных ифинансовыхвычислений,постоянно сталкиваются с подобнымипроблемами, поэтому C#предоставляет другиетипы с плавающей точкойдляработы с оченьбольшими иочень малыми значениями: Ì В типеfloat может храниться любое число от ±1.5 × 10 –45 до±3.4×10 38 с 6–9значащими цифрами. Ì doubleпозволяет хранить вещественныечисла от ±5.0 ×10 – 324 до±1.7 ×10 308 с 15–17 значащимицифрами. Ì В типеdecimal может храниться любое число от ±1.0 ×10 –28 до±7.9×10 28 с 28–29значащими цифрами. Если вашей программе приходится рабо- тать с денежными суммами, всегда стоит использовать тип decimal для хранения числа. Типыfloat и double называются типами «с плавающей точкой», потому что точка может перемещаться внутри числа (в отличие от чисел «сфиксированной точкой», у которых дробная часть всегда состоит из постоянного количества цифр). Этот факт — а на самом деле и многие аспекты, относящиеся к числам с плавающей точ- кой, особенно точность, — вы гл ядит немного странно, поэтому стоит немного пояснить происходящее. «Значащие цифры» представляют точность числа: 1 048 415, 104.8415 и 0.000001048415 содержат 7 значащих цифр. Таким образом, когда мы говорим, что тип float может хранитьбольшие числа до3.4 × 1038 или малые числа до –1 .5 × 10–45 , это означает,чтоонможетхранитьчислаиз8цифр, за которыми следуют 30 нулей, или числа из 37 нулей, за которыми следу- ют 8 цифр. Типы float и double также могут иметь специальные значения, включая по - ложительный и отрицательный нуль, положительную и отрицательную бес- конечность, а также специальное значение NaN (не число), которое пред- ставляет... в общем, значение, которое вообще не является числом. Также они содержат статические методы и позволяют проверять эти специальные значения. Попробуйте выполнить следующий цикл: for (float f = 10; float.IsFinite(f); f *= f) { Console.WriteLine(f); } Теперь попробуйте выполнить тот же цикл с double: for (double d = 10; double.IsFinite(d); d *= d) { Console.WriteLine(d); } Числа с плавающей точкой под увеличительным стеклом Если вы уже давно не пользовались экспо- ненциальной записью, 3.4 x 1038 означает чис- ло 34, за которым идут 37 нулей, а –1 .5 x 10-45 означает –00 ... (еще 40 нулей)... 0015. Тип decimal обес- печивает суще- ственно более высокую точность (больше значащих цифр), поэтому он лучше подходит для финансовых вычислений.
дальше 4 201 типы и ссылки Иногда объявление переменной и присваивание ей значения объединяются в одну команду: int i = 37; но вы уже знаете, что инициализировать переменную при объявлении не обязательно. Что произойдет, если использовать перемен- ную без присваивания значения? Давайте проверим! Воспользуйтесь интерак- тивным окном C# (или консолью .NET, если вы работаете на Mac), чтобы объ- явить переменную и проверить ее значение. Откройте интерактивное окно C#(из менюView>>Other Windows) или выполните команду csi из терминала Mac. Объявите каждую переменную, после чего введи- те имя переменной, чтобы просмо- треть ее значение по умолчанию. Запишите значение по умолчанию для каждого типа в обозначенном месте. 0 Мы записали первый ответ за вас. Поговорим о строках Выужеписаликод,работающийсо строками. Что же именно представляетсобойстрока? В любом приложении .NET строка является объектом. Ее полноеимя класса имеет вид System.String — иначе говоря, класс обладает именем String и принадлежит к пространству именSystem (как и класс Random, который использовалсяранее).Используя ключевое слово C# string, вы на самом деле ра- ботаете с объектами System.String. Более того, можно заменить string на System.String во всем коде,написанном вами до настоящего момента,и онвсеравнобудетработать!(Ключевоесловоstring называется синонимом — с точки зрения кода C# string и System.String означают одно и то же.) Такжесуществуютдва специальных строковых значения: пустая строка "" (или строка без символов) иnull,т. е. строка,которой вообщеничего не присвоено. Мы вернемся кnull позднеев этойглаве. Строкисостоятиз символов,а конкретно из символовЮникода (об этом выузнаетебольшевэтойкни- ге).Иногда требуется сохранить впрограммеотдельныйсимвол(например,Q,j или$); втакихслучаях используетсятип char. Литеральные значенияcharвсегда заключаются водинарныекавычки('x', '3'). Такжеводинарных кавычкахмогут содержаться служебныепоследовательности: '\n' — разрывстроки, '\t' — табуляцияи т.д.) .ДлязаписислужебнойпоследовательностивкодеC#используютсядва символа, но ваша программа храниткаждую служебную последовательность впамятикак одинсимвол. Наконец,существуетеще более важный типobject.Если переменнаяимеет тип object, ейможноприсвоить любое значение. Ключевое слово object также является синонимом — оно эквивалентно System.Object. Возьми в руку карандаш int i; long l; float f; double d; decimal m; byte b; char c; string s; bool t;
202 глава 4 л итера л int i; long l; float f; double d; decimal m; byte b; char c; string s; bool t; 0 0 0 ' \0' n ull false 0 0 0 Если вы использовали командную строку на Mac или в системах Unix, возможно, вы видели, что в качестве значения по умолчанию для char ис- пользуется '\xO' вместо '\O'. Что это значит, мы объясним позднее, когда речь пойдет о Юникоде. Литерал ¦ значение, записанное непосредственно в вашем коде Литерал — число, строка или другоефиксированное значение, включенное вваш код. Выуже неодно- кратнопользовались литералами;приведемлишьнесколько примеровчисел, строкидругихлитералов, встречавшихсяв книге: int number = 15; string result = "the answer"; public bool GameOver = false; Console.Write("Enter the number of cards to pick: "); if (value == 1) return "Ace"; Такимобразом,когда вызаписываетекомандуint i = 5;, вэтойкоманде5 являетсялитералом. Использование суффиксов для определения типа литерала Когда вы записываете в Unity команды следующего вида: InvokeRepeating("AddABall", 1 .5F , 1); возникает вопрос: для чего нуженсуффиксF? Авызаметили, что безF в литералах1.5F и0.75F программа строиться небудет? Этосвязаностем,что у литералов есть тип. Каждомулитералу тип назначается автоматически, ав C#существуют специальные правилаотносительнообъединения разныхтипов. Вы можете самостоятельно увидеть, как работает этот механизм. Добавьтеследующую строкув любую программуC#: int wholeNumber = 14.7; Припопыткепостроить программуIDEвыдаетследующеесообщениеобошибке вокне ErrorList: IDEсообщает о том, что улитерала14.7имеется тип, — это double. Припомощи суффикса можно изменить его тип. Например, попробуйте преобразовать его во float, добавив суффикс F вконце (14.7F),илив decimal добавлением суффиксаM(14.M — кстати, буква «M» обозначает«money», т. е. «деньги»).Теперь всообщенииобошибкеговорится,что программенеудаетсяпреобразоватьfloat или decimal. Добавьте D (или полностью опустите суффикс), и ошибка исчезнет. А вы сможете найти все литералы в этих командах из кода, написанного в предыду- щей главе? Последняя команда содержит два литерала. C# предполага- ет, что целочис- ленный литерал без суффикса (например, 371) представляет int, а литерал с де- сятичной точ- кой (например, 27.3) представ- ляет double. Возьми в руку карандаш Решение
дальше 4 203 типы и ссылки ВC#существуют десяткизарезервированных слов,называемыхключевыми словами.Эти слова, зарезервированные компиляторомC#, немогут использоватьсяв качестве именпеременных.Мно- гие ключевые слова вам уже известны — ниже приведена краткая сводка, которая поможет вам закрепить ихв мозгу.Напишите,что,повашему мнению,делает каждое изэтихключевыхслов в C#. Если вам непременно хочется использовать зарезервированное ключевое слово в качестве имени переменной, поставьте перед ним @, но ближе подойти к зарезервированному слову компилятор не позволит. То же самое при желании можно делать и с незарезервированными именами. na mesp ace for clas s new us ing if wh ile e lse Возьми в руку карандаш
204 глава 4 решени е ВC# существуют десятки зарезервированных слов,называемых клю- чевыми словами.Этислова,зарезервированные компилятором C#,не могут использоваться в качестве имен переменных.Многие ключевые словавам уже известны —ниже приведена краткая сводка,которая поможетвам закрепитьих вмозгу.Напишите, что,повашему мнению,делает каждое изэтихключевых слов вC#. Все классы и методы программы принадлежат некоторому пространству имен. Пространства имен гарантируют, что имена, которые вы используете в своей про- грамме, не будут конфликтовать с именами из .NET Framework или других классов. Определяет цикл, заголовок которого выполняет три команды. Сначала объяв- ляется используемая переменная, следующая команда проверяет переменную по заданному условию. Третья команда делает что-то со значением. Классы содержат методы и поля и используются для создания экземпляров объектов. Поля содержат информацию, известную объекту, а методы опреде- ляют поведение объекта. Блок кода, начинающийся с else, должен следовать сразу же за блоком if. Он вы- полняется, если условие предшествующей команды if ложно. Используется для создания нового экземпляра объекта. Используется для перечисления всех пространств имен, используемых в вашей программе. Команда using позволяет использовать классы из разных частей .N ET Framework. Один из способов организации условной логики в программе. Если условие ис- тинно, то выполняется один блок; если ложно — делается что-то другое. Циклы while продолжают работать, пока условиев начале цикла остается истинным. na mesp ace for clas s new u sing if wh ile e lse Возьми в руку карандаш Решение
дальше 4 205 типы и ссылки Переменные как емкости для данных Вседанныезанимаютместовпамяти. (Помнитекучуиз предыдущейглавы?)Входе своейработывампридетсядумать отом, сколькоместа вам потребуетсядляхранения строкииличиславпрограмме. Этооднаиз причин дляиспользованияпеременных— онипозволяют выделить достаточныйобъем памятидляхранения ваших данных. Представьте переменную в виде чашки или другой емкости для хранения данных. ВC#используютсяразныечашкидляхраненияразныхтиповданных. Какизвестно, вкофейнях существуютчашкиразныхразмеров; точно так же существуютразные размерыпеременных. long int short byte 64бита 32бита 16бит 8бит Столько битов памяти выделяется для перемен- ной при ее объявлении. Тип byte вмещает целые числа до 255, а тип long способен хра- нить числа в диапазоне многих миллиардов миллиардов. Не все данные хранятся в куче. Типы значений обычно хранят свои данные в дру- гой части памя- ти, называемой стеком. Об этом будет подробно рассказано позд- нее. Тип short вмеща- ет целые числа до 32 767. Тип int часто используется для целых чисел. Он позво- ляет хранить числа до 2 147 483 647. Тип long ис- пользуется для дей- ствитель- но больших целых чисел. Класс Convert может использоваться для анализа битов и байтов Вы наверняка слышали, что все программирование сводится к манипуляциям с 0 и1.В .NET существует статическийклассConvertдляпреобразованиямеждуразнымитипамиданных.Вос- пользуемсяим для рассмотренияпримера, которыйдемонстрирует, какработаютбитыибайты. Битпринимает значения1или0.Байт состоит из8битов,так что байтоваяпеременнаясодер- жит8-битноечисло,т. е. число, котороеможетбыть представлено 8битами.Как это выглядит? ВоспользуемсяклассомConvert для преобразованиядвоичныхчиселв байты: Convert.ToByte("10111 ", 2) // возвращает 23 Convert.ToByte("11111111 ", 2); // возвращает 255 Байты могутхранить числа от0 до 255,потомучто онииспользуют8битпамяти— 8-разрядное число представляется двоичным числом в диапазоне от 0 до 11111111(илиот0 до 255в деся- тичнойсистеме). Тип short хранит 16-битные значения. Воспользуемся Convert.ToInt16 для преобразования дво- ичного значения111111111111111(15единиц) вshort. Значениеintявляется32-битным, такчто мы воспользуемся Convert.ToInt16 для преобразования 31 единицы в int: Convert.ToInt16("111111111111111", 2); // возвращает 32767 Convert.ToInt32("1111111111111111111111111111111" , 2); // возвращает 2147483647 Преобразуйте! В первом аргументе ConvertToByte передается преобразуемое число, а во втором — основание системы. Двоич- ные числа используют основание 2.
206 глава 4 большие значения занимают больше памяти Другие типы тоже могут иметь разные размеры Числас дробнойчастью хранятсянетак, какцелыечисла, иразныетипыс плавающейточкой занимают разные объемы памяти. Для большинства чисел с дробной частью можно пользо- ваться типом float— наименьшимтипом данныхдляхраненияцелыхчисел. Есливам потребу- ется более высокая точность, используйтеdouble. Есливы пишетефинансовоеприложение, в котором должны храниться денежные величины, всегда стоит использовать тип decimal. Да,и последнее: никогда неиспользуйтеdoubleдля денежных сумм — только decimal. float double decimal 32 бита 64 бита 128 бит Эти типы предназначены для дробных величин. Боль- шие переменные способ- ны представлять больше знаков в дробной части. Мы уже говорили о строках, и вы знаете, что компилятор C#умеетработать с символами и нечисловыми типами. Тип char рассчитан на один символ, а строка содержит множество «сцепленных»символов.Дляобъекта строкинесуществуетфиксированногоразмера —он рас- ширяетсяпоразмерамтехданных,которые выхотите внем хранить. Тип данныхbool исполь- зуетсядля хранениязначенийtrue илиfalse вроде тех, которые выиспользоваливкомандахif. Строки могут быть большими... ОЧЕНЬ большими! В C# для хранения длины строки используется 32-битное число, поэтому максимальная длина строки равна 231 (bkb 2 147 483 648) символов. bool char string 8 16 зависит от размера строки В C# также предусмотрены типы для хра- нения данных, которые не пред- назначены для числовой инфор- мации. Разные типы с плавающей точкой зани- мают разные объемы па- мяти: float – наименьший, decimal – наибольший.
дальше 4 207 типы и ссылки Трикомандыв следующем спискене построятся—либопотому,чтомыпытаем- ся втиснуть слишком многоданных в маленькую переменную,либопотому, что данные несоответствуют типам переменных.Обведитеэтикомандыизапишите краткое объяснение того,чтоже сниминетак. 10 литров в 5-литровой банке Когда вы объявляете переменную с некоторым типом, компиля- торC#выделяет(илирезервирует) всю память, необходимую для хранениямаксимального значенияэтоготипа. Дажеесли значение далекоот верхней границы объявленноготипа, компилятор видит чашку,вкоторойхранятсяданные,а ненаходящеесяв нейчисло. Такимобразом,следующаяпопытка несработает: int leaguesUnderTheSea = 20000; short smallerLeagues = leaguesUnderTheSea; 20000поместитсявтипеshort,непроблема. Нотак какперемен- наяleaguesUnderTheSea объявлена стипомint, C#воспринимает ее какимеющуюразмерintисчитает, что эта переменнаяслишком велика для хранениявконтейнереshort.Компиляторне позволит выполнять подобные преобразованиянаходу.Выдолжныследить за тем,чтобы для данных,с которыми вы работаете, выбирался подходящий тип. int hours = 24; short y = 78000; bool isDone = yes; short RPM = 33; int balance = 345667 - 567; string taunt = "your mother"; byte days = 365; long radius = 3; char initial = 'S'; string months = "12"; 20.000 i n t s hort Все, что видит C#, — эт о попытка сохранить int в short (что сделать не удастся). Значение, которое хранится в int, роли не играет. И это логично. Что, если позднее вы сохрани- те в int большее значе- ние, которое не поме- стится в short? Так что C# на самом деле пыта- ется вам помочь. Возьми в руку карандаш
208 глава 4 преобразования типов Трикомандыв следующем списке не построятся —либо потому, чтомы пыта- емся втиснуть слишком много данныхв маленькую переменную, либо потому, чтоданные не соответствуют типам переменных. Обведите эти команды иза- пишите краткое объяснение того, чтоже снимине так. short y = 78000; bool isDone = yes; byte days = 365; В переменной byte хранят- ся только значения от 0 до 255. Для такого значения потребуется short. Тип short может хра- нить числа от -32 767 до 32768. Число слишком большое! Переменной bool можно присваивать только значе- ния true и false. Приведение типов позволяет копировать значения, которые C# не может автоматически преобразовать к другому типу Посмотрим, что происходит при попытке при- своить значение decimal переменнойint. Создайте новый проект консольного приложения и добавьте следующий код в метод Main: float myFloatValue = 10; int myIntValue = myFloatValue; Console.WriteLine("myIntValue is " + myIntValue); 1 Попробуйте построить свою программу.Вы получите туже ошибкуCS0266, которуювиделиранее: Присмотритеськпоследнимсловамсообщенияобошибке: «(возможно,выпропустилиприведение типа?)». КомпиляторC#дает по-настоящемуполезнуюподсказкуотом, какможнорешить проблему. 2 Чтобы устранить ошибку, выполните приведение (casting)decimal вint. Для этого тип, ккоторомупреобразуетсязначение,заключаетсявкруглыескобки: (int).После того как вы измените строку к приведенному нижесостоянию, программабудет успешно компилироваться ивыполняться: int myIntValue = (int) myFloatValue; 3 Что же произошло? КомпиляторC#не позволяетприсвоить значение переменной неподходящего типа —даже если значение может поместитьсявпеременной!Каквыясняется, МНОГИЕошибкивозникают из-за проблемс типами, и компилятор вам помогает, подталкивая в нужном направлении. Когда вы используете приведение типов, фактическивы говорите компилятору: да, я знаю, что типы разные, но гарантирую, что в этом конкретном случаеданныепоместятся вновой переменной. Здесь значение decimal приводится к типу int. Когда значение с плавающей точкой приводится к int, оно округляется в нижнюю сторону до ближайшего целого числа. Неявное преобразование означает, что C# может ав- томатически преобразовать значение к другому типу без потери информации. Возьми в руку карандаш Решение Сделайте это!
дальше 4 209 типы и ссылки По ссылкеможно найти гораздо больше информации о разных типах значений в C# — поверьте, она заслуживает вашего внимания: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/value-types Когда вы выполняете приведение типа со слишком большим значением, C#усекает его до размеров нового контейнера Выуже видели,что значениеdecimalможетбыть приведенок int. Каквыясняется,любое число можнопривестиклюбому другомучисловомутипу.Однако этоне означает, что значениеостанется неизменным в результате приведения. Допустим,имеется переменная int, которой присвоено значение365. Если вы хотите привестиее к типуbyte(максимальное значение255), то вместо ошибки произойдет циклический возврат. 256при приведении к byte дает значение 0,257пре- образуется в1,258преобразуется в2 и так далее до 365,котороепреобразуется в109.Когда вы снова доберетесь до 255, преобразование снова «сбрасывается» до 0. Есливыиспользуете+ (или*,/или–)с двумяразнымичисловымитипами,оператор автомати- чески преобразует меньший тип к большему. Пример: int myInt = 36; float myFloat = 16.4F; myFloat = myInt + myFloat; Так как значение int может поместиться в float, но float в int не поместится, оператор + преоб- разует myIntк типу float передтем,какприбавлять его кmyFloat. Но это не означает, что любой тип можно привестик любому другому типу. Создайте новый проект консольного приложе- ния и введите эти команды в метод Main. За- тем постройте программу —она выдает много ошибок. Вычеркните команды,для которых вы- водятся ошибки.Это поможет вам определить, какие приведения типов возможны, а какие нет! int myInt = 10; byte myByte = (byte)myInt; double myDouble = (double)myByte; bool myBool = (bool)myDouble; string myString = "false"; myBool = (bool)myString; myString = (string)myInt; myString = myInt.ToString(); myBool = (bool)myByte; myByte = (byte)myBool; short myShort = (short)myInt; char myChar = 'x'; myString = (string)myChar; long myLong = (long)myInt; decimal myDecimal = (decimal)myLong; myString = myString + myInt + myByte + myDouble + myChar; Возьми в руку карандаш
210 глава 4 конкатенация и преобразование Циклический возврат своими руками! В «циклическом возврате» при при- ведениях числовых типов нет ничего загадочного — вы можете легко про- делать все самостоятельно. Открой- те любое приложение-калькулятор с функцией Mod (вычисление остатка от деления — иногда поддерживается только в инженерном режиме) и вычис- лите 365 Mod 256. Да! Когда вы выпол няе т е конкатенацию, C# преобразует значения. Когда вы используете оператор + для объединения строки с другим значением,этоназываетсяконкате- нацией.Приконкатенациистроки с int, bool,float или другим типом значенияэто значение автоматиче- скипреобразуется. Такие преобра- зованияотличаютсяот приведения типов, потому что во внутренней реализации для значения вызыва- ется метод ToString... а .NET среди прочегогарантирует,чтокаждый объект содержит метод ToString, который преобразует его встроку (но каждый отдельный класс сам определяет, имеет ли смысл эта строка). Я объединял числа и строки в своих сообщенияхеще с момента работы с циклами в главе 2! Выходит, я все это время занимался преобразованиями типов? Произвольный тип не всегда можно при- вести к любому другому типу. Создайте новый проект консольного приложения и введите эти команды в метод Main. Затем постройте программу — она выдает много ошибок. Вычеркните команды, для которых выводятся ошибки. Это поможет вам опре- делить, какие приведения типов возможны, а какиенет! int myInt = 10; byte myByte = (byte)myInt; double myDouble = (double)myByte; bool myBool = (bool)myDouble; string myString = "false"; myBool = (bool)myString; myString = (string)myInt; myString = myInt.ToString(); myBool = (bool)myByte; myByte = (byte)myBool; short myShort = (short)myInt; char myChar = 'x'; myString = (string)myChar; long myLong = (long)myInt; decimal myDecimal = (decimal) my Lo ng; myString = myString + myInt + myByte + myDouble + myChar; Возьми в руку карандаш Решение
дальше 4 211 типы и ссылки часто Зад авае м ые вопросы Существуют две важные разновидности преобразований, которые не требуют приведения типов. Первое — автоматическое преобразование, которое выполняется каждыйраз, когда вы используете арифметическиеоператоры, какв следующем примере: long l = 139401930; short s = 516; doubled=l -s; d=d/123.456; Console.WriteLine("The answer is " + d); Другой вариант автоматического преобразованиятиповвстречается при использовании оператора + для конкатенации строк(что означает обычноеприсоединение одной строкик концу другой, как это делалось дляоконсообщений).Когда вы используете оператор +для конкатенациистрокис данными, относящимися к другому типу, оператор автоматически преобразует числа в строки. Ниже приведен пример — попробуйте добавить эти строки в любую программуC#. Первые две строки выполняются нормально,но третья некомпилируется: long number = 139401930; string text = "Player score: " + number; text = number; Компилятор C# выдает следующую ошибку в третьей строке: ScoreText.text является строковым полем, так что при использовании оператора + для конкатенации строкизначениебыло присвоено нормально. Но когдавы пытаетесьприсвоитьемузначение напрямую, C# не имеет возможности автоматически преобразовать значение long в string. Чтобы преобразовать его в строку, вызовите для него метод ToString. Оператор - вычитает short из long, а оператор = преобразу- ет результат в double. C# выполняет некоторые преобразования автоматически В:Вы использовалиметоды Convert.ToByte, Convert.ToInt32 и Convert.ToInt64для преобразова- ния строк с двоичными числами вцелые числа. А можно ли преобразоватьцелые числа обратно в двоичные? О: Да, можно .КлассConvertсодержит метод Convert.ToString, который преобразуетразныетипы значенийв строки. ОкноIntelliSense показ ыва ет, как он раб ота ет: Таким образом, Convert.ToString(255, 2)возвращает строку "11111111" ,а Convert.ToString(8675309, 2)возвращает строку "10000100010111111101101" — поэ кс периментируйте, чтобы лучше понять, как работаютдвоичные числа.
212 глава 4 c# преобразует некоторые типы автоматически При вызове метода аргументы должны быть совместимы с ти пами параметров Впредыдущей главе мы воспользовались классомRandom длявыбораслучайного числа от1 до 5(невключая), при помощикоторого выбиралась масть карты: int value = random.Next(1, 5); Попробуйте заменить первый аргумент1 на1.0: int value = random.Next(1.0, 5); Литерал double передается методу, который рассчитывает получить значение int. А значит, вас не удивит, что компилятор откажется строить программу — вместо этого онвыдаетошибку: Иногда C# может выполнить преобразование автоматически. C# не знает, как преобразоватьdouble вint(например,1.0 в1),но знает,какпреобразоватьint вdouble(например,1в1.0).Говоря конкретнее: Ì КомпиляторC# знает,какпреобразовать целоечисло в число с плаваю- щей точкой. Ì А ещеон знает,как преобразовать целыйтип вдругойцелыйтип илитип с плавающейточкойв другойтип с плавающейточкой. Ì Но эти преобразования он может выполнить только в том случае, если преобразуемый тип имеет одинаковый или меньшийразмер, чем тип, к которому выполняется преобразование. Таким образом, C# может пре- образоватьintвlongилиfloatвdouble,ноне сможетпреобразоватьlong вint или double вfloat. НоRandom.Next —не единственныйметод, которыйвыдает ошибкикомпилятора,есливы попытаетесь передать переменную,тип которой несоответствует параметру.Это относится ко всемметодам — даже тем, которыенаписаны вами. Добавьтеследующий методв консольноеприложение: public int MyMethod(bool add3) { int value = 12; if (add3) value += 3; e lse value -= 2; return value; } Попробуйте передать методу string илиlong — вы получите ошибкуCS1503, вкоторой говорится о не- возможности преобразованияаргумента вbool.Некоторые люди немогут запомнить, чем параметры отличаются от аргументов. Давайтеразберемся: Параметр — то, что вы определяете в своем методе. Аргумент — то, что вы передаете методу. Методу с параметром int можно передать аргумент byte. Когда компиля- тор выдает сооб- щение об ошибке «недопустимый аргумент», это означает, что вы попытались при вызове метода передать пере- менные, типы которых не соот- ветствуют пара- метрам метода.
дальше 4 213 типы и ссылки ¢ Существуют типы значений для переменных, ко - торые могут содержать числа разных размеров. Для самых больших целых чисел стоит использовать тип long, а самые малые (до 255) могут объявляться с ти- пом byte. ¢ Каждый тип значения обладает определенным разме- ром. Значение большего типа невозможно поместить в меньшую переменную независимо от фактического размера данных. ¢ Когда вы используете литеральные значения, ис- пользуйте суффикс F для обозначения типа float (15.6F) и суффиксM для типа decimal (36.12M). ¢ Используйте тип decimal для денежных сумм. Точ- ность формата с плавающей точкой... не идеальна. ¢ Некоторые типы C# умеет преобразовывать авто- матически (неявное преобразование): short в int, int в double, float в double. ¢ Если компилятор не позволяет присвоить переменной значение другого типа, необходимо применить при- ведение типов. Чтобы привести значение к другому типу, укажите целевой тип в круглых скобках перед значением. ¢ В языке существуют зарезервированные ключевые слова, которые не могут использоваться в качестве имен переменных. Эти слова (for, while, using, new и т. д .) решают конкретные задачи в языке. ¢ Параметр — то , что вы определяете в методе. Аргу- мент — то, что вы передаете методу. ¢ При построении кода IDE использует компилятор C# для преобразования кода в исполняемую программу. ¢ Методы статического класса Convert используются для преобразования значений междуразнымитипами. КЛЮЧЕВЫЕ МОМЕНТЫ В: В последней командеif используется короткое условие if (add3).Это то же самое, что if (add3 == true)? О: Да. Взг лян ите еще ра з на кома нду if/else: if (add3) value += 3; el se value -= 2; Команда if всегда проверяет условие на истинность. Так как переменная add3 относится к типу bool, при выполнении онадает результат true или false. Следовательно, явно включать проверку == true не обязательно. Такжеможнопроверитьусловие наложность,длячегоиспользуется оператор ! (логическое отрицание, или оператор NOT). Запись if (!add3) эквивалентна if (add3 == false). В дальнейших примерах кода при условной проверке логиче- ских переменных мы обычно используем запись if (add3) или if (!add3), а не используем == для явной проверки истинности ил и ло жнос ти переме нной. В: Также вблоках if и else отсутствуют фигурные скобки. Означает лиэто, что их можно не указывать? О: Да — но только если блок if или else состоит из одной команды. В данном случае фигурные скобки{ } можно опустить, потому что блок if состоит из одной команды {return 45;}, как иблок else {return 61;}. Если позднее вы захотитедо- бавить в такой блок еще одну команду, его придется заключить в фигурные скобки: if (ad d3) value += 3; else { Con sol e.W rit eLi ne(" Sub tra cti ng 2") ; value -= 2; } Дажееслифигурныескобкиможноопустить, будьте осторожны— существует высокийрискнаписать код, которыйделает не то, чего вы ожидали. От фигурных скобок вреда никогда не будет, но вам стоит привыкнуть к видукоманд ifс фигурными скобками ибез них. часто Зада вае мые вопросы
214 глава 4 оуэн хочет улучшить свою игру Оуэн постоянно старается улучшить свою игру... Хорошие гейм-мастера стремятся создать уигроковнаилучшие впечатле- нияотигры.ГруппаОуэна собираетсяначать новую кампанию сновыми персонажами, иОуэн думает, что снебольшимиизменениямивформуле определенияхарактеристик игра станетболееинтересной. ФОРМУЛА ОПРЕДЕЛЕНИЯ ХАРАКТЕРИСТИК * Начните с броска 4d6, чтобы получить число от 4до24. * Разделите результат броска на 1.75. * Прибавьте 2 к результа- ту деления. * Округлите до ближайшего целого. * Если результат слишком мал, используйте мини- мальное значение 3. «Бросок 4d6» озна- чает, что вы броса- ете четыре обычных шестигранных кубика и складываете резуль- таты. Когда игроки заполняют свои листы персонажей в начале игры, они выполняют следующие действия для вычисления каждой из начальных характеристик своего персонажа: Стандартные правила игры хороши для начала, но я уверен, что можно и лучше.
дальше 4 215 типы и ссылки .. . но процесс проб и ошибок может занимать много времени Оуэнэкспериментировал с различными вариантами настройки вычисления характери- стик. Онуверен, что формула в целом хороша, но ему хотелось бы иметь возможность экспериментировать с числами. р а з д е л и т ь н а 1 . 7 5 ? а м о ж е т , н а 3 . 1 ? н е С т о и т л и в ы б р а т ь м и н и м у м 1 ? и л и 2 ? в ы ч и т а т ь 5 ? Оуэнунравится общаяформула:бросить4d6,разделить, вычесть, округлить,использовать минимальноезначение... но онне уверен вправильностиконкретныхчисел. Мне кажется, что 1.75 маловато для деления результата броска... И возможно, к результату лучше прибавить 3, а не 4. Наверняка должен быть более удобный способ проверки всех этих идей! Чтомы можемсделать, чтобы помочь Оуэну в поиске оптимальной комбинации значений для обновленной форм улы х арак теристик ? Мозговой штурм
216 глава 4 поможем Оуэну Поможем Оуэну в экспериментах с характеристиками В следующем проекте мы построим консольное приложение .NET Core, при помощи которогоОуэнсможет протестировать свою формулу вычисления характеристик сраз- ными значениямии проверить,как онивлияютна результат. Формула получаетчетыре входных значения: начальный бросок4d6;делитель, на который делится результат; при- ращение,которое прибавляется крезультату деления,и минимум, который используется, еслирезультат окажетсяслишком маленьким. Оуэнвводит четыре входных значения в приложении, котороебудет вычислять характе- ристикипоэтим данным. Вероятно, онзахочет протестировать наборразных значений, поэтому дляудобства приложениебудет снова иснова запрашивать входные данные,пока приложение небудетзавершено, отслеживать значения, введенныеприкаждойитерации, ииспользовать предыдущиезначения по умолчанию при следующей итерации. Вотчто долженвидеть Оуэн призапускеприложения: Формула определения характериСтик * Начните с броска 4d6, чтобы получить чис- лоот4до24. * Разделите результат броска на 1.75. * Прибавьте 2 к ре- зультату деления. * Округлите до бли- жайшего целого. * Если результат слишком мал, исполь- зуйте минимальное значение 3. Приложение запрашивает различные значения, используемые для вычис- ления характеристик. Значения по умолчанию выводятся в квадратных скобках (например, [14] или [1.75]). Оуэн может ввести значение или просто нажать Enter, чтобы под- твердить значение по умолчанию. Здесь Оуэн опробует новые значения: результат броска делится на 2.15 (вместо 1.75), результат деления увеличивается на 5 (вместо 2), и при вычислении используется минимальное значение 2 (вместо 3). С исходным бро- ском 14 будет получено значение 11. Теперь Оуэн хочет проверить те же значения при другом исходном броске 4d6, поэтому он вводит 21 на первый запрос и нажимает Enter, чтобы подтвердить значения по умолчанию, сохра- ненные приложением при предыдущей итерации. На этот раз будет получено значение 14. Страница из кни ги гейм- мастера с формулой вычисления характери- сти к. Этот проект больше предыдущего консольного приложения, которое вы построили, поэтому мы рассмотрим его за несколько этапов. Сначала мы разберемся в коде вычисления характеристики, затем будет написан остальной код приложения, и наконец, займемся диагностикой ошибок в коде. Итак, за дело!
дальше 4 217 типы и ссылки Мыпостроиликласс,которыйпоможетОуэнуввычислениихарактеристик. Чтобы использоватьего, необходимозадатьзначенияегополейStarting4D6Roll, DivideBy, AddAmountиMinimum (илиоставить полям значения, заданныеприобъявлении) и вызвать метод CalculateAbilityScore. К сожалению, в одной строке кода допуще- на ошибка. Обведите строку сошибкойинапишите, чтоснейне так. class AbilityScoreCalculator { public int RollResult = 14; public double DivideBy = 1.75; public int AddAmount = 2; public int Minimum = 3; public int Score; public void CalculateAbilityScore() { // Результат броска делится на значение поля DivideBy double divided = RollResult / DivideBy; // AddAmount прибавляется к результату деления int added = AddAmount += divided; // Если результат слишком мал, использовать значение Minimum if (added < Minimum) { Score = Minimum; } else { Score = added; } } } После тогокак выпометите строку кода спроблемой,запишите,какие проблемы вы внейобнаружили. Удастся ли вам обнаружить про- блему, не вводя класс в IDE? Най- дете ли вы стро- ку, из-за которой компилятор вы- дает сообщение об оши б ке? Эти поля инициализи- руются значениями из формулы вычисления характеристик. При- ложение использует их при выводе значений по умолчанию. Подсказка: сравните ком- ментарии в коде с формулой вычисления характеристик на странице из книги Оуэна. Какая часть формулы отсут- ствует в комментариях? Возьми в руку карандаш
218 глава 4 попробуем решить проблему Использование компилятора C# для поиска проблемной строки кода Создайте проект консольного приложения .NET Core Console App с именем AbilityScoreTester. Затем добавьте классAbilityScoreCalculator с кодом из упражнения «Возьмивруку карандаш». Есликод был введен правильно,вы получитеошибку компилятора C#: Каждыйраз, когда компилятор C# выдает сообщение об ошибке, тщательно прочитайте его. Обычно внем присутствует подсказка,которая поможет обнаружить проблему.В данном случаепричина точно обозначена: компилятор не может преобразоватьdouble вintбезприведениятипа.Переменнаяdivided объявлена с типомdouble, но C# не позволит добавить еек полю int(такому, как AddAmount), потому что незнает,как преобразовать ее. Компилятор дает чрезвычайно ценную подсказку о том, что вы должны выполнить приведение типа double-переменной divided, прежде чем прибавлять ее кint-полю AddAmount. Добавим приведение типа, чтобы класс AbilityScoreCalculator компилировался... Теперь мы знаем, вчем заключается суть проблемы,и можем добавить приведение типа дляисправле- ния проблемной строки кода в AbilityScoreCalculator. Ошибка «Не удается неявно преобразовать тип» выдается следующей строкой: int added = AddAmount += divided; Ошибка возникаетиз-за того,что команда AddAmount+=dividedвозвращает значение double, котороене можетбыть присвоеноint-переменнойadded. Проблему можно решить приведением divided кint, чтобы при прибавлении кAddAmount было воз- вращено другое значениеint. Замените в этой строке кода divided на (int)divided: int added = AddAmount += (int)divided; Приведение также добавляет отсутствующую часть формулыОуэна: *Округлите дО ближайшегО целОгО. Когда вы приводитеdoubleкint,C# округляетрезультат— такчто,например,(int)19.7431D дает19.До- бавляяэтоприведение,вы также добавляетепунктформулы,отсутствующий вклассе. . .. н о ошибка все равно осталась! Работа ещенезакончена!Ошибка компилятора исправлена,такчто проектуспешностроится.Но хотя компиляторC# не протестует, проблема все еще осталась. Удастся ли вам найти ошибку в следующей строкекода? Ошибка компилятора C# буквально напоминает о том, что вы могли пропустить приведение типа. Преобразуйте! Похоже, заполнять ответ во врезке «Возьми в руку карандаш» еще рано!
дальше 4 219 типы и ссылки Завершим построение консольного приложения, использующего классAbilityScoreCalculator. В этом упражнении мы предоставим метод Main для консольного приложения. Ваша задача — написать код двух методов: метод ReadInt читает ввод от пользователя и преобразует его в int вызовом int. TryParse, а метод ReadDouble делает то же самое, но работает со значениями double вместо int. 1. Добавьте следующий методMain. Почти весьего код ужезнакомвам попредыдущим проектам.Единственным нов- шествомоказываетсявызов методаConsole.ReadKey: char keyChar = Console.ReadKey(true).KeyChar; Метод Console.ReadKey читает одно нажатие клавиши сконсоли. При пере- даче аргумента true ввод перехватывается и не выводится на консоль.Сце- пленныйвызов .KeyChar возвращает нажатие клавишив видеchar. Итак,перед вами полныйметод Main—добавьте егов программу: static void Main(string[] args) { AbilityScoreCalculator calculator = new AbilityScoreCalculator(); while (true) { calculator.RollResult = ReadInt(calculator.RollResult, "Starting 4d6 roll"); calculator.DivideBy = ReadDouble(calculator.DivideBy, "Divide by"); calculator.AddAmount = ReadInt(calculator.AddAmount, "Add amount"); calculator.Minimum = ReadInt(calculator.Minimum, "Minimum"); calculator.CalculateAbilityScore(); Console.WriteLine("Calculated ability score: " + calculator.Score); Console.WriteLine("Press Q to quit, any other key to continue"); char keyChar = Console.ReadKey(true).KeyChar; if ((keyChar == 'Q') || (keyChar == 'q')) return; } } 2. Добавьте метод с именем ReadInt. Метод получает два параметра: сообщение для пользователя и значение по умолчанию.Сообщение выводится наконсоль,за ним следует значение по умолчанию в квадратныхскобках. Затем метод читает строку сконсоли ипытается преобразовать ее. Если преобразование проходит успешно,то метод использует этозначение;в противном случае используетсязначениепо умолчанию. /// <summary> /// Выводит сообщение и читает значение int с консоли. /// </summary> /// <param name="lastUsedValue">Значение по умолчанию.</param> /// <param name="prompt">Сообщение, выводимое на консоль.</param> /// <returns>Прочитанное значение int или значение по умолчанию, если преобразование /// невозможно.</returns> static int ReadInt(int lastUsedValue, string prompt) { // Вывести сообщение, за которым следует [значение по умолчанию]: // Прочитать строку из ввода и попытаться преобразовать ее вызовом int.TryParse // Если преобразование прошло успешно, вывести на консоль строку " using value" + value. // В противном случае вывести на консоль строку " using default value" + lastUsedValue } 3. Добавьте метод ReadDouble, которыйполностью повторяет ReadInt,но использует double.TryParse вместо int.TryParse. Метод double.TryParse работает так же, как int.TryParse, но его переменная out должна относиться к типуdouble вместо int. Мы будем использовать один экземпляр AbilityScoreCalculator. Пользовательский ввод будет обновлять поля экземпляра, чтобы он запоминал значения по умолчанию для следующей итерации цикла while. Упражнение
220 глава 4 ошибка в коде Ниже приведены методы ReadInt и ReadDouble, которые выводят сообщение со значением по умолчанию, читают строку с консоли, пытаются преобразовать ее в int или double и выводят на консоль сообщение с преобразованным значением или со значением по умолчанию. static int ReadInt(int lastUsedValue, string prompt) { Console.Write(prompt + " [" + lastUsedValue + "]: "); string line = Console.ReadLine(); if (int.TryParse(line, out int value)) { Console.WriteLine(" using value " + value); return value; } else { Console.WriteLine(" using default value " + lastUsedValue); return lastUsedValue; } } static double ReadDouble(double lastUsedValue, string prompt) { Console.Write(prompt + " [" + lastUsedValue + "]: "); string line = Console.ReadLine(); if (double.TryParse(line, out double value)) { Console.WriteLine(" using value " + value); return value; } els e { Console.WriteLine(" using default value " + lastUsedValue); return lastUsedValue; } } Вызов double.TryParse рабо- тает так же, как версия для int, не считая того, что для выходной переменной должен использоваться тип double. Осно вател ь но р азб ери тесь в том, как каждая итерация циклаwhile в методе Main сохраняет в полях значения, введенные пользователем, а затем использует их как значения по умолчанию для следующей итерации. Спасибо за ваше приложение! Мне не терпится опробовать его в деле. Упражнение Решение
дальше 4 221 типы и ссылки Starting 4d6 roll [14]: 18 using value 18 Divide by [1.75]: 2.15 using value 2.15 Add amount [2]: 5 using value 5 Minimum [3]: using default value 3 Calculated ability score: 13 Press Q to quit, any other key to continue Starting 4d6 roll [18]: using default value 18 Divide by [2.15]: 3.5 using value 3.5 Add amount [13]: 5 using value 5 Minimum [3]: using default value 3 Calculated ability score: 10 Press Q to quit, any other key to continue Starting 4d6 roll [18]: using default value 18 Divide by [3.5]: using default value 3.5 Add amount [10]: 7 using value 7 Minimum [3]: using default value 3 Calculated ability score: 12 Press Q to quit, any other key to continue Starting 4d6 roll [18]: using default value 18 Divide by [3.5]: using default value 3.5 Add amount [12]: 4 using value 4 Minimum [3]: using default value 3 Calculated ability score: 9 Press Q to quit, any other key to continue Starting 4d6 roll [18]: using default value 18 Divide by [3.5]: using default value 3.5 Add amount [9]: using default value 9 Minimum [3]: using default value 3 Calculated ability score: 14 Press Q to quit, any other key to continue Что-то не так . Класс должен запоминать значения, которые я ввел, но он не всегда это делает. В от! При первой итерации я ввел приращение 5. Все остальные значения были сохранены правильно, но для приращения приложение выводит значение по умолчанию 10. Ты прав, Оуэн. В коде ошибка. Оуэнхочетопробоватьразные значениядлясвоей формулывычисления характеристик, поэтомумы в циклезапрашиваемэти значенияснова иснова. ЧтобыОуэнубыло проще изменять значения по одному, мы включили в приложениефункцию, котораязапоминаетвведенныеранеезначенияи предлагаетих какзначенияпоумолчанию. Данная возможность былареализована хранением класса AbilityScore CostCalculator и обновлением его по- лей прикаждой итерациицикла while. Ивсежес приложением что-то не так . Большин- ство значений запоминается нормально, но для приращениязапоминаетсяошибочноечисло. При первой итерации Оуэн ввел 5, но в качестве зна- чения по умолчанию предлагается 10. Затем он вводит 7,но получает значениепоумолчанию12. Что происходит? Какие шаги мы можем предпринять для выявления ошибки в калькуля- торе харак терис тик ? Мозговой штурм Откуда вообще взялось число 9? Мы видели его прежде? Можно ли сде- лать из этого какие-то выводы о причинах ошибки? Стр ан но. На предыдущей итерации Оуэн ввел прираще- ние 5, но про- грамма выдает значение по умолчанию 10. И снова: послед- нее приращение было равно 7, но приложение выводит значе- ние по умолча- нию 12. Непо- нятно. Результаты работы приложения.
222 глава 4 диагностика ошибок в отладчике Наконец-то мы можем исправить ошибку в приложении Оуэна Теперь мызнаем,чтопроизошло, и можемисправитьошибку —изменение оказываетсянезначительным . Нужнолишьизменить команду, чтобы в нейиспользовался оператор +вместо +=: int added = AddAmount + (int)divided; Замените += на +, чтобы строка кода не обновляла переменную AddAmount . «Элементарно», как говорил Шерлок. В процессе отладки кода вы выполняете детективнуюработу. В приложении что-то вызывает ошибку, и ваша задача — определить подозреваемых и пройти по их следам. Проведем небольшое расследование в духе Шерлока Холмса и по- смотрим, удастсяли нам найти виновника. Похоже, проблема существует только с приращением, поэтому для начала найдем любую строку с упоминанием поля AddAmount — установите в нем точку прерывания: А вот еще одна строка в методеAbilityScoreCalculator.CalculateAbilityScore — еще один подозреваемый: Теперь запустите программу. Когда в методе Main сработает точка прерывания, выберите calculator.AddAmount и добавьте отслеживание (если просто щелкнуть правой кнопкой мыши на AddAmount и выбрать команду Add Watch в меню, то будет добавлено отслеживание для AddAmount, но не calculator.AddAmount). Что-нибудь выглядит странно? Ничего необычного не видно. Значение читается и обновляется вполне нормально. Видимо, проблема вызвана чем-то другим — отключите или удалитеэту точку прерывания. Продолжайте выполнять программу. При достижении точки прерывания AbilityScoreCalculator.CalculateAbilityScore до- бавьте отслеживаниедля AddAmount.По формуле Оуэна эта строка кода должна прибавлять AddAmount к результату деления. Выполните командув пошаговом режиме, и ... ?! Стоп, что?! Значение AddAmount изменилось. Но... такого не должно быть — это невозможно! Верно? Как говорил Шерлок Холмс, «если исключить невозможное , тогда то, что останется , будет ответом, каким бы невероят- ным он ни казался». Похоже, мы обнаружили источник проблемы. Команда должна привести divided к int и округлить до целого числа, а затем прибавить результат к AddAmount и сохранить результат в added. Но у нее также имеется непредвиденный побочный эффект: она обновляет AddAmount суммой, потому что в команде используется оператор +=, который не только возвращает сумму, но и присваивает ее AddAmount. По следу Эта команда должна обновлять переменную "added", но не из- менять поле AddAmount.
дальше 4 223 типы и ссылки Мы построили класс, который поможет Оуэнуввычислениихарактеристик. Чтобы использоватьего,необходимозадать значенияегополей Starting4D6Roll, DivideBy, AddAmount и Minimum(или оставить полям значения, заданные при объявлении) и вызвать метод CalculateAbilityScore. К сожалению, в однойстрокекода допуще- наошибка. Обведитестрокусошибкой и напишите, что сней нетак. int adde d = AddAmount += divided; После тогокак выпометите строку кода спроблемой,запишите,какие проблемы вы внейобнаружили. Во-первых, она не компилируется, потому что результат AddAmount += divided имеет тип double и для присваивания его int потребуется приведение типа. Во- вторых, в ней используется += вместо +, из-за чего строка обновляет AddAmount. Итак, причина ошибки найдена, и мы наконец-то можем привести решение. В:Явсееще недо концапонимаю, чем оператор + отличается от оператора +=. Как они работают и в каких случаях нужно использовать тот или иной оператор? О:Некоторые операторы могут объединяться со знаком =. К их числу относятся += длясложения, - =длявычитания,/=дляделения, *=дляумножения и%=длявычисленияостатка.Операторы,работа- ющие с двумя значениями (такие, как +), называются бинарными. С бинарными операторами можно выполнять так называемое комбинированное присваивание. Иначе говоря, вместо: a=a+c; можно использовать запись: a+=c; Это означает то же самое. Если вы предпочитаете техническое объяснение, комбинированноеприсваивание x op=y эквивалентно x=xopy. Операторы, объединяющие бинарный оператор со знаком = (такие, как += или *=), называются комбини- рованными операторами присваивания. В:Нокак тогда обновляется переменная added? О:Вся путаница в калькуляторе произошла из-за того , что опе- ратор присваивания = тоже возвращает значение. Вы можете использовать команду вида: intq=(a=b+c) Эта команда, как обычно, вычисляет a = b + c. Оператор = возвра- щает значение, поэтому переменная q также будет обновлена результатом. А значит, команда: int added = AddAmount += divided; эквивалентна следующей: int added = (AddAmount = AddAmount + divided); В результатеAddAmount увеличивается на divided, но результат также сохраняется в added. В:Погодите, что? Оператор присваивания возвращаетзначение? О:Да, = в озвращаетприсвоенное значение. Следовательно, в коде int first; int second = (first = 4); и first, и second в итоге будет присвоено4.Откройте консольное приложение ипроверьтес помощью отладчика. Этодействительно раб ота ет! Оператор += прика- зывает C# вычислить a + с, а затем сохра- нить результат в a. Возьми в руку карандаш Решение Часто задаваемые вопросы
224 глава 4 странные числа с плавающей точкой Точно. Типdecimal обеспечивает куда большую точность, чем double или float, поэтому проблемы 0.30000000000000004 в нем не существует. Некоторые типы с плавающей точкой — не только в C#, но и в большинстве языков программирования! — могут создавать редкие и странныеошибки. Как в результате сложения 0.1 + 0.2 можно получить 0.30000000000000004? Оказывается, некоторыечисла просто неимеют точного представления в виде double — это связано со способомиххраненияв двоичныхданных(0и1в памяти). Например, .1D не точноравно .1 .Попробуйтеумножить .1D * .1D — вы полу- чите 0.01000000000000002 вместо 0.01 . С другой стороны, .1M * .1M дает точный ответ. Типыfloat и double очень полезны для многих операций(например, для позиционированияGameObject вUnity).Есливамнужнабольшая точность,как, например, в финансовых приложениях, работающих с денежными суммами, — выбирайте decimal. Эй, детка!Хочешь увидеть нечто по-настоящему странное? Попробуйте включить следующую команду if/else в консольное приложение: if (0.1M + 0.2M == 0.3M) Console.WriteLine("They're equal"); else Console.WriteLine("They aren't equal"); Под второй командой Console появляется зеленая волнистая черта — это преду- преждение обобнаружениинедоступногокода.КомпиляторC#знает, что 0.1+0.2 всегда даетрезультат0.3,так что код никогда не достигнет части else в команде. Выполните код — он выводит на консоль сообщение They’re equal. Теперь замените литералыfloat на double(помните: такиелитералы,как0.1,по умолчанию интерпретируются как double): if (0.1 + 0.2 = = 0.3) Console.WriteLine("They're equal"); el se Con sole .Wr iteL ine ("T hey are n't equ al" ); Происходит что-то странное: предупреждение переместилось в первую строку командыif.Попробуйтевыполнитьпрограмму.Этого не можетбыть!На консоль выводится сообщениеTheyaren’t equal.Как0.1+0.2 можетбыть неравно0.3? Атеперь еще одно. Замените0.3на0.30000000000000004(с15 нулямимежду3и4). Теперь программа снова выводит They’re equal.Очевидно, 0.1Dплюс0.2Dравно 0.30000000000000004 . Вот, значит, почему для финансовых вычислений всегда нужно использовать decimal, а не double? Что-что?! Преобразуйте!
дальше 4 225 типы и ссылки Пример 0.1D + 0.2D != 0.3D является граничным случаем, т. е. проблемой, которая возникает только в конкретной редкой ситуации — например, когда параметр принимает одно из крайних значений (очень большое или очень малое число). Если вы захотите больше узнать об этом, Джон Скит (John Skeet) написал отличную статью о хранении чисел с плавающей точкой в памяти .NET. Статья доступна по адресу https://csharpindepth.com/Articles/FloatingPoint. Джон предоставил нам ряд ценнейших технических за- мечаний по первому изданию книги, которые были очень важны для нас. Спасибо, Джон! В: Я все еще плохо понимаю, чем преобразования от- личаются от приведения типов. Можно объяснить чуть понятнее? О: Преобразование — общий, универсальный термин для переводаданных из одного типа вдругой.Приведение— куда болееконкретнаяоперация,с явнымиправиламотносительно того, какие типы могутбыть приведены кдругим типамичто делать, если данные значения одного типа не полностью совпадают с типом, к которому осуществляется приведение. Пример такого правила вам уже встречался — когда число с плавающей точкой преобразуется к int, оно округляется усечениемдробной части. Еще одно правило проявляется при циклическом возврате дляцелочисленных типов: если число не помещается в целевом типе, оно усекается с ис- пользованием оператора вычисления остатка. В: В формуле Оуэна мы делили два значения, а затем округляли результат до ближайшего целого. Как это со- гласуется с приведением типов? О: Допустим, у вас имеется набор значений с плавающей точ ко й: float f1 = 185 .26F; double d2 = .0000316D; decimal m3 = 37.26M; и вы хотите привести их к типу int, чтобы присвоить их пере- менным inti1,i2 и i3.Мы знаем,чтов переменныхint могут храниться только целые числа, так что программа должна что-то сделать с дробной частью числа. ПоэтойпричинеC#руководствуетсяуниверсальнымправилом: дробная часть отбрасывается.f1 преобразуется в185,d2 — в0, аm3 — в37.Впрочем,неверьтенамна слово —напишите собственный код C#, который преобразует эти три значения с плавающей точкой вint, и посмотрите, что произойдет. Существует целый веб-сайт, посвященный проблеме 0.30000000000000004! Откройте сайт https://0.30000000000000004.com, чтобы увидеть примеры на многих других языках. Задаваемые вопросы часто
226 глава 4 ссылки похожи на наклейки О б ъ е к т G u y No 2 Использование ссылочных переменных для обращения к объектам Создавая новыйобъект, высоздаетеэкземпляркомандой new— например, newGuy()ввашейпрограмме вконцепредыдущейглавы создаетновыйобъектGuyв куче. Приэтомкобъектувсеравнонужно как-то обратиться,длячегоиспользуютсятакиепеременные,как joe: Guyjoe= newGuy().Давайтеразберемся втом,что же здесь происходит. Команда new создала экземпляр, но одного создания экземпляра недостаточно. Понадобится ссылка на объект.Поэтомумысоздалиссылочную переменную, т. е .переменную типаGuyсименем(например,joe). Такимобразом, joeсодержит ссылкуна толькочтосозданныйобъектGuy.Каждыйраз, когдавызахотите использовать этот конкретный объектGuy, к нему можно обратитьсяпо ссылочной переменнойjoe. Любаяпеременная, относящаясякобъектномутипу, является ссылочнойпеременной, т. е .она содержит ссылкунаконкретныйобъект. Простоубедимсявтом, что мы одинаковопонимаем этитермины, потому что они будут часто использоваться в этой главе. Воспользуемся первыми двумя строками программы «Джо иБоб» изпредыдущей главы: static void Main(string[] args) { Guyjoe=newGuy(){Cash=50,Name="Joe"}; Guy bob = new Guy() { Cash = 100, Name = "Bob" }; Куча перед вы- полнением кода. В ней ничего нет. Куча после выполне- ния кода. Она содер- жит два объекта; переменная «joe» ссылается на один объект, а перемен- ная «bob» — на дру- г ой. К объекту Guy можно обратиться только одним спо- собом: по ссылочной переменной с име- нем «bob». Создание ссылки выглядит так, словно вы пишете имя на на- клейке и прикрепляете ее к объ- екту. Надпись становится своего рода «меткой», по которой вы можете обращаться к объекту в будущем. Ссы ло чная п ерем енная . О б ъ е к т G u y No 1 joe bob Создает объект, на который будет указывать п ерем енн ая.
дальше 4 227 типы и ссылки Вероятно, навашей кухне есть контейнерыдлясоли и сахара. Если вы случайнопоменяетеместами наклейки, врядлиедуможнобудет есть — хотя надписи изменились, содержимое контейнеров оста- лось прежним. Ссылки похожи на эти наклейки. Наклейки можно перемещать, но набор доступных методов и данных зависит от объекта,ане отссылки — иссылкиможно копироватьточно так же, как вы копируете значения. Ссылки напоминают наклейки на ваших объектах Мы создали объ- ект Guy клю- чевым словом «new» и скопи- ровали ссылку на него опера- тор ом =. В этомконкретном случае существуетмножество разных ссылок на объектGuy, по- тому что многие разные методы используют его для разных целей.Каждой ссылке присваивается отдельноеимя, котороеимеет смысл вэтом контексте. Вотпочему можетбыть очень полезноиметь несколько ссылок, указывающих наодинэк- земпляр.Следовательно,впрограммуможно включить командуGuy dad=joe,а затем вызвать методdad.GiveCash().Есливы хотите написатькод, работающийсобъектом, вам понадобится ссылкана этот объект.Безссылкиобращениякобъекту невозможны. Каждая из этих меток представляет собой отдельную ссылочную переменную, но все они указывают на ОДИН И ТОТ ЖЕ объект Guy. Ссылка представляет собой своего рода метку, которая ис- пользуется в вашем коде для обращений к конкретному объ- екту. Она использу- ется для обращения к полям и вызова методов того объ- екта, на который она указывает. joe joseph dad u n c l e J o e y b r o t h e r h e y Y o u m i s t e r c u s t o m e r Guy joe = new Guy(); Guy joseph = joe; О б ъ е к т G u y
228 глава 4 О б ъ е к т G u y No 1 Если ни одной ссылки не осталось, объект уничтожается сборщиком мусора Еслисобъектабыла снята последняянаклейка, программа не сможет обратить- сяк объекту.Этоозначает,что C#может пометить объект длясборки мусора. ПослеэтогоC#избавляется от любыхобъектов, на которыенесуществует ни однойссылки,и освобождаетпамять,которуюзанималиэтиобъекты. “ Joe” 50 joe Теперь создадим второй объект. Послеэтого мы имеем два объекта Guy и две ссылочные пере- менные: однапеременная(joe)дляпервогообъектаGuy идругая переменная(bob)для второго. Guy bob = new Guy() { Cash = 100, Name = "Bob" }; 2 Код, создающий объект. Навсякийслучайвспомним,о чемговорилосьранее: когдавы используете команду new,вы тем самым приказываетеC#соз- дать объект. Когда вы берете ссылочную переменную(напри- мер,joe)и присваиваетеей созданный объект,все выглядит так,словно вы прикрепляете кнему новую наклейку. Guyjoe=newGuy(){Cash=50,Name="Joe"}; 1 О б ъ е к т G u y No 1 “ Joe” 50 joe О б ъ е к т G u y No 2 “Bob” 75 bob Для создания этого объ- екта Guy использовался инициализатор объекта. Поле Name содержит строку "Joe", полю Cash присвоено значение int 50, а ссылка на объект со- храняется в переменной с именем «joe». Мы создали другой объ- ект Guy и переменную с именем «bob», кото- рая указывает на него. Переменные похожи на наклейки — это простые метки, которые можно «прикрепить» к любому объ ек ту. об ъекты пр евр ащаю тся в м усор
дальше 4 229 типы и ссылки Чтобы объект оставался в куче, должны существовать ссылки на него. Через какое-то время после исчезновения последней ссылки исчезает и сам объект. БАХ! Ссылок на первый объект Guy больше не осталось... поэт ому он унич т ожа ет ся с борщиком мус ора. Теперь,когдапеременнаяjoeуказываетна тотжеобъект,что иbob,на объектGuy,на которыйонауказываларанее,неоста- лосьни одной ссылки.Что же происходит? C#помечает объект длясборки мусораи современемуничтожает его. Бах—и его нет! 4 CLR отслеживает всессылки на каждый объект и при исчезновении последней ссылки помечает его для уничтожения. Но возможно, у CLR сейчас есть более неотложные дела, поэтому объект может просуществовать еще несколько миллисекунд — и дажеболее! После того как CLR (см. далее в интервью «От- кровенно о сборке мусо- ра»!) удалит последнюю ссылку на объект, он помечается для сборки мусора. О б ъ е к т G u y No 2 “Bob” 75 joe bob Возьмем ссылку на первый объект Guy и переведем ее на вт орой объе кт Guy. Внимательно присмотримся к тому, что происходит при создании но- вого объекта Guy. Мыберем переменную и используем оператор при- сваивания =, чтобы задать ей новое значение— вданном случаессылку, которую возвращает команда new. Присваивание работает, потому что ссылкиможно копироватьточно так же, как выкопируетезначения. Попробуем скопировать это значение: joe = bob; Команда сообщает C#, что переменная joe должна указывать на тот же объект,что иbob. Теперь переменные joe и bob указывают на один объект. 3 О б ъ е к т G u y No 1 “Jo e” 50 О б ъ е к т G u y No 2 “Bob” 75 bob joe
230 глава 4 множественные ссылки О б ъ е к т D o g No 1 О б ъ е к т D o g No 3 О б ъ е к т D o g No 1 О б ъ е к т D o g No 2 При перемещении ссылочных переменных не- обходимо действовать осторожно. Во многих случаяхсоздается впечатление, что переменная простоначинаетуказывать надругойобъект.Тем не менее при этом можетбытьудалена последняя ссылка надругойобъект. Этоневсегда плохо,но может бытьи не тем,чеговыожидали.Взгляните: Множественные ссылки и их побочные эффекты О б ъ е к т D o g No 1 Dog rover = new Dog(); rover.Breed = "Greyhound"; 1 Dog fido = new Dog(); fido.Breed = "Beagle"; Dog spot = rover; 2 Dog lucky = new Dog(); lucky.Breed = "Dachshund"; fido = rover; 3 rover — объект Dog, у которого поле Breed содержит Greyhound. fido — другой объект Dog, а spot — всего лишь еще одна ссылка на первый объект. lucky — третий объект. fido теперь указывает на объект No 1. Таким об- разом, на объект No 2 не остается ни одной ссылки. С точки зрения програм- мы работа закончена. Objects:______ References:_____ Objects:______ References:_____ Objects:______ References:_____ 1 1 2 3 2 4 БАХ! spot rover fido rover l u c k y fido rover spot publi c parti al clas s Dog { public void Ge tPet() { Conso le.Writ eLine(" Woof!") ; } } Dog Breed
дальше 4 231 типы и ссылки Dog rover = new Dog(); rover.Breed = "Greyhound"; Dog rinTinTin = new Dog(); Dog fido = new Dog(); Dog greta = fido; 1 Dog spot = new Dog(); spot.Breed = "Dachshund"; spot = rover; 2 Dog lucky = new Dog(); lucky.Breed = "Beagle"; Dog charlie = fido; fido = rover; 3 Objects:______ Re fe re nc e s:_____ Objects:______ Re fe re nc e s:_____ rinTinTin = lucky; Dog laverne = new Dog(); laverne.Breed = "pug"; 4 Objects:______ Re fe re nc e s:_____ charlie = laverne; lucky = rinTinTin; 5 Objects:______ Re fe re nc e s:_____ Objects:______ Re fe re nc e s:_____ Теперь ваша очередь. Ниже приведен одиндлинный блок кода.Опреде- лите, сколько объектов и ссылок существует на каждой стадии. Справа нарисуйте схемус представлением объектов иссылок в куче. Возьми в руку карандаш
практическая работа со ссылками Dog rover = new Dog(); rover.Breed = "Greyhound"; Dog rinTinTin = new Dog(); Dog fido = new Dog(); Dog greta = fido; 1 Dog spot = new Dog(); spot.Breed = "Dachshund"; spot = rover; 2 Dog lucky = new Dog(); lucky.Breed = "Beagle"; Dog charlie = fido; fido = rover; 3 Objects:______ Ref e re nc e s:_____ Objects:______ Ref e re nc e s:_____ rinTinTin = lucky; Dog laverne = new Dog(); laverne.Breed = "pug"; 4 Objects:______ Ref e re nc e s:_____ charlie = laverne; lucky = rinTinTin; 5 Objects:______ Ref e re nc e s:_____ Objects:______ Ref e re nc e s:_____ 3 3 4 7 4 8 4 8 4 5 О б ъ ек т D o g No 5 D o g o b j e c t # 3 О б ъ ек т D o g No 1 О б ъ ек т D o g No 4 О б ъ ек т D o g No 1 О б ъ ек т D o g No 3 О б ъ ек т D o g No 5 О б ъ ек т D o g No 4 О б ъ ек т D o g No 2 О б ъ ек т D o g No 3 О б ъ ек т D o g No 1 О б ъ ек т D o g No 4 О б ъ ек т D o g No 3 О б ъ ек т D o g О б ъ ек т D o g No 2 О б ъ ек т D o g No 3 Создается один новый объект Dog, но spot содержит единствен- ную ссылку на него. Когда spot присваи- вается ссылка rover, этот объект про- падает. charlie была присвое- на ссылка fido, когда ссылка fido еще указы- вала на объект No 3. После этого ссылка fido была переведена на объект No 1. На этом этапе ссылки переме- щаются, но новые ссылки не создаются. Присваивание lucky ссылки tinTinTin не делает ничего, по- тому что обе ссыл- ки уже указывают наодинитотже об ъе кт. О б ъ ек т D o g No 1 О б ъ е к т D o g No 2 БАХ! Объект Dog No 2 потерял свою последнюю ссыл- ку, поэтому он уничтожается. Когда rinTinTin переходит на объект lucky, старый объ- ект rinTinTin исчез. l u c k y fido rover greta r i n T i n T i n l a v e r n e r i n T i n T i n rover s p o t fido greta r i n T i n T i n rover s p o t fido ch arlie greta rover s p o t fido r i n T i n T i n l u c k y charlie greta rover s p o t fido l a v e r n e c ha rlie greta r i n T i n T i n l u c k y Возьми в руку карандаш Решение 232 глава 4
дальше 4 233 типы и ссылки HeadFirst:Хорошо, мыпонимаем, чтовы решаете для нас довольно важную задачу.Можетечуть под- робнеерассказать, чемжевы занимаетесь? CLR (Common Language Runtime): В общем-то все просто: явыполняюваш код.Каждыйраз, когда выза- пускаете приложение.NET, яобеспечиваюегоработу. HeadFirst:Что вы имеетев виду— «обеспечиваю работу»? CLR:Яберунасебявсю низкоуровневую «черную работу»,становясьсвоего рода «посредником»меж- дувашейпрограммойи компьютером,на котором она выполняется. Когда речь заходит о создании экземпляров или сборке мусора, именно я выпол- няю всеэти операции. Head First: И как же именно это происходит? CLR: Когда вы запускаете программу вWindows, Linux, macOSили другой операционной системе, ОСзагружаеткоднамашинномязыке издвоичного файла. Head First: Извините, вынужден перебить. А вы можетенемного отступить ирассказать,чтотакое машинный язык? CLR:Конечно. Программа,написаннаяна машин- ном языке, состоит из кода, выполняемого непо- средственно процессором, — и читать его намного сложнее,чем кодC#. HeadFirst: Если процессор выполняетреальный машинныйкод, то что делаетОС? CLR:ОС следитза тем, чтобы каждой программе был выделен отдельный процесс, чтобы она со- блюдала правила безопасности системы, а также предоставляетразличныеAPI. Head First: Для наших читателей, которые не зна- ют,что такое API?.. CLR:API, илиинтерфейсприкладного программи- рования, — это набор методов, представляемыхОС, библиотекойилипрограммой.APIОС позволяют выполнять такие операции, какработасфайловой системойили взаимодействие соборудованием.Но порой они достаточно сложны в использовании (особенноAPIуправленияпамятью) иизменяются взависимости от ОС. HeadFirst:Хорошо,возвращаемся ктеме.Выупо- мянулио двоичном файле. Что это такое? CLR: Двоичный файл (обычно) создается компи- лятором — программой, задача которой сводит- ся к преобразованию высокоуровневого языка в низкоуровневый код — например, машинный. Двоичные файлы Windows обычно завершаются суффиксом .exe или .dll. HeadFirst:Что-тоздесь не так.Высказали: «низко- уровневыйкод— например, машинный». Означает ли это, что существуют и другиеразновидности низкоуровневого кода? CLR:Точно. Яне используютот же машинный язык, что и процессор. Когда вы строите ваш код C#, VisualStudio приказывает компилятору C# гене- рироватькодна языкеCIL(CommonIntermediate Language).Именноегояи выполняю.Код C#преоб- разуется вкодCIL,а ячитаю ивыполняю этот код. HeadFirst:Выупомянулиобуправлениипамятью. Какоеместовнейзанимаетсборка мусора? CLR:Да!Среди прочего, яреализую очень, очень полезную возможность — я слежу за тем, когда ваша программа завершаетработу с некоторыми объектами. И когда объектстановится ненужным, я уничтожаю его, чтобы освободить занимаемую им память. Когда-то программистам приходилось делать это самостоятельно, но благодаря мне вам теперь не придетсябеспокоитьсяоб этом. Может, вы об этом и незнали, но я сильно упрощаю вашу задачу по изучению C#. Head First: Вы упоминали двоичные файлы Windows. А если я запускаю программы .NET на MacиливLinux? В этихОСвы делаете тоже самое? CLR: Если вы используете macOS или Linux или запускаетеMono в Windows, то формально вы ис- пользуете не меня, а моего родственника Mono Runtime. Онреализуеттотже стандартECMACLI (Common Language Infrastructure), что и я. Таким образом,когда дело доходит до того,о чем я гово- рилранее,мы оба делаем одно и то же. Откровенно о сборке мусора Интервью недели: .NET CLR
234 глава 4 Создайте программу с классом Elephant. Создайте два экземпляра Elephant, после чего по - меняйте местами ссылки, которые указывают на них, — т ак , чтобы ни один из экземпляров не был уничтожен в ходе сборки мусора. Ниже показано, как должен выглядеть результат при выполнении программы. Мы построим новое консольное приложение, которое содержит класс с именем Elephant. Пример вывода программы: Press 1 for Lloyd, 2 for Lucinda, 3 to swap You pressed 1 Calling lloyd.WhoAmI() My name is Lloyd. My ears are 40 inches tall. You pressed 2 Calling lucinda.WhoAmI() My name is Lucinda. My ears are 33 inches tall. You pressed 3 References have been swapped You pressed 1 Calling lloyd.WhoAmI() My name is Lucinda. My ears are 33 inches tall. You pressed 2 Calling lucinda.WhoAmI() My name is Lloyd. My ears are 40 inches tall. You pressed 3 References have been swapped You pressed 1 Calling lloyd.WhoAmI() My name is Lloyd. My ears are 40 inches tall. You pressed 2 Calling lucinda.WhoAmI() My name is Lucinda. My ears are 33 inches tall. Elephant Name EarSize WhoAmI CLR уничтожает в ходе сборки мусора любой объект, на который не осталось ни одной ссылки. Поэтому вот вам подсказка для этого упражнения: если вы хотите перелить чашку кофе в другую чашку, наполненную чаем, вам понадобится третья чашка, в кото- рую можно перелить чай... Диаграмма класса Elephant, который вам предстоит соз- дать. КлассElephant содержит метод WhoAmI, который выводит на консоль две строки со значениями полей Name и EarSize. Перестановка ссылок заставляет пере- менную lloyd вызвать метод объекта Lucinda, и наоборот. Повторная пере- становка возвраща- ет ситуацию к ис- ходному состоянию на момент запуска программы. Упражнение меняем местами элементы
дальше 4 235 типы и ссылки Ваша задача — создать консольное приложение .NET Core с классом Elephant, структура ко- торого соответствует диаграмме класса. Поля и методы класса должны выводить показанный результат. Создайте новое консольное приложение .NET Core и добавьте класс Elephant. Добавьте впроект классElephant.Взгляните на диаграммуклассаElephant —вампонадобится поле int с именем EarSize и поле string с именемName. Добавьте их иубедитесь в том, что оба поля объявлены открытыми(public).Затем добавьтеметодс именем WhoAmI,который выводит на консоль две строки со значениями полейName и EarSize. Чтобы понять, как именно должен выглядеть результат,просмотритевывод примера. Создайте два экземпляра Elephant и ссылку. Воспользуйтесь инициализаторами объектов для создания двух объектов Elephant: Elephant lucinda = new Elephant() { Name = "Lucinda", EarSize = 33 }; Elephant lloyd = new Elephant() { Name = "Lloyd", EarSize = 40 }; Вызовите их методы WhoAmI. Когда пользователь нажимает 1, вызовите lloyd.WhoAmI. Когда пользователь нажимает 2, вызовитеlucinda.WhoAmI.Убедитесь втом,что результат совпадает с приведенным вкниге. А теперь самое интересное: поменяйте местами ссылки. Самаяинтереснаячастьупражнения: когда пользователь нажимает 3, приложение должновы- звать метод,которыйменяет местамидвессылки. Этот методдолжнынаписать вы. После того какссылкипоменяются местами,принажатии1 на консоль должно выводиться сообщение объектаlucinda, а при нажатии2 — сообщениеобъектаlloyd.Приповторнойперестановке ссылок вседолжно вернутьсяк нормальномусостоянию. 1 2 3 4 О б ъ е к т Ele p h a n t No 1 О б ъ е к т E leph a n t No 2 lloyd lucinda О б ъ е к т Ele p h a n t No 1 О б ъ е к т E l eph a n t No 2 lloyd lucinda О б ъ е к т Ele p h a n t No 1 О б ъ е к т E leph a n t No 2 lloyd lucinda Когда пользователь нажимает 3, приложе- ние меняет местами две ссылки, так что ссылка lucinda теперь указывает на объект Elephant, на который раньше указывала ссылка lloyd, и наоборот. Теперь при вызове lloyd.WhoAmI() должно выводиться сообще- ние "My name is Lucinda". Если пользователь снова нажмет 3, приложение снова меняет ссылки местами. При вызове lloyd.WhoAmI() снова выводится сообщение "My name is Lloyd". П о м е н я т ь ! П о м е н я т ь ! Упражнение
236 глава 4 две ссылки на один объект Создайте программу с классом Elephant. Создайте два экземпляра Elephant, после чего по - меняйте местами ссылки, которые указывают на них, — т а к , чтобы ни один из экземпляров не был уничтожен в ходе сборки мусора. Класс Elephant: class Elephant { public int EarSize; public string Name; public void WhoAmI() { Console.WriteLine("My name is " + Name + "."); Console.WriteLine("My ears are " + EarSize + " inches tall." ); } } А это метод Main класса Program: static void Main(string[] args) { Elephant lucinda = new Elephant() { Name = "Lucinda", EarSize = 33 }; Elephant lloyd = new Elephant() { Name = "Lloyd", EarSize = 40 }; Console.WriteLine("Press 1 for Lloyd, 2 for Lucinda, 3 to swap"); while (true) { char input = Console.ReadKey(true).KeyChar; Console.WriteLine("You pressed " + input); if (input == '1') { Console.WriteLine("Calling lloyd.WhoAmI()"); lloyd.WhoAmI(); } else if (input == '2') { Console.WriteLine("Calling lucinda.WhoAmI()"); lucinda.WhoAmI(); } else if (input == '3') { Elephant holder; holder = lloyd; lloyd = lucinda; lucinda = holder; Console.WriteLine("References have been swapped"); } else return; Console.WriteLine(); } } При объявлении переменной holder команда new не используется, потому что создавать дополнительный экземпляр Elephant не нужно. Если просто присвоить lloyd ссылку lucinda, то в программе не оста- нется ни одной ссылки на объект Lloyd и объ- ект будет потерян. Вот почему необходима дополнительная пере- менная (мы назвали ее «holder») для хранения ссылки на объект Lloyd, пока эта ссылка не бу- дет присвоена lucinda. Elephant Name Ea rS ize WhoAmI Упражнение
дальше 4 237 типы и ссылки Помимо потери последней ссылки на объект, наличие нескольких ссылок может привестик непреднамеренному изменению объекта. Иначе говоря, одна ссылкана объектможет изменить объект, тогда какдругаяссылканаэтот объект понятия неимеетобэтихизменениях.Посмотрим, какэто работает. Добавьте еще один блок «elseif» в метод Main. Сможете ли вы предпо- ложить,что произойдетприего выполнении? else if (input == '3') { Elephant holder; holder = lloyd; lloyd = lucinda; lucinda = holder; Console.WriteLine("References have been swapped"); } else if (input == '4 ') { lloyd = lucinda; lloyd.EarSize = 4321; llo yd.W hoA mI( ); } els e { re tu rn; } Атеперь запуститесвою программу.Вот что выувидите: You pressed 4 My name is Lucinda My ears are 4321 inches tall. You pressed 1 Calling lloyd.WhoAmI() My name is Lucinda My ears are 4321 inches tall. You pressed 2 Calling lucinda.WhoAmI() My name is Lucinda My ears are 4321 inches tall. Посленажатия 4 и выполнения нового кода переменныеlloyd иlucinda содержат одну и ту же ссылку на второйобъектElephant. Принажатии1 для вызова lloyd.WhoAmI выводится точно такое же сообщение, как при нажатии2 для вызова lucinda.WhoAmI.Перестановка нина что невлияет, потому что вы меняетеместамидвеодинаковые ссылки. Две ссылки ¦ ДВЕ переменные, по которым можно изменять данные одного объекта Эта команда присваивает Ear Siz e з начен ие 4321 у того объекта, на который указывает ссылка, хранящаяся в переменной lloyd. БАХ! О б ъ е к т E le p h a n t No 1 О б ъ е к т E lep h a n t No 2 lloyd О б ъ е к т E lep h a n t No 2 Послеэтой команды переменные lloyd и lucindaуказывают на ОДИН объект Elephant. Когда вы меняете местами эти две наклейки, ничего не изменится, потому что они прикреплены к одному объекту. lucinda luc in da lloyd Программа ведет себя нормально... пока вы не нажмете 4. После этого при нажатии 1 или 2будет выводиться один и тот же результат, а при нажатии 3 для перестановки ссылок ничего происходить не будет. А поскольку ссылка lloyd уже не указывает на первый объект Elephant, она уничтожается сбор- щиком мусора... и вер- нуть ее уже не удастся! Сделай те это!
238 глава 4 общение объектовElephant Добавьте в класс Elephant метод для отправки сообщения. Теперь в класс Elephant необходимо включить метод SpeakTo. В этом методе ис- пользуетсяспециальное ключевое словоthis.Эта ссылка позволяет объектуполу- чить ссылку на самого себя. public void SpeakTo(Elephant whoToTalkTo, string message) { whoToTalkTo.HearMessage(message, this); } Повнимательнееприсмотримсяк тому, что здесь происходит. При вызове метода SpeakTo объекта Lucinda:: lucinda.SpeakTo(lloyd, "Hi, Lloyd!"); будет вызван метод HearMessage объекта Lloyd: whoToTalkTo.HearMessage("Hi , Lloyd!", this); 2 Объекты используют ссылки для взаимодействия друг с другом Досихпорформы взаимодействовали с объектами, используя ссылочные переменные для вызова своих методов и проверки их полей.Объекты также могут вызывать методы другихобъектов по ссылкам. Собственно, форма не можетсделать ничего такого,чегобы немогли сделать вашиобъекты,потому что форма тоже является объектом. Когда объекты взаимодействуют друг с другом,онимогут использовать полезноеключевое слово this. Каждыйраз, когда объект использует ключевое слово this, он ссылается на самого себя, т. е. эта ссылка указывает на тот объект, в котором она используется. Чтобы вылучшепоняли,чтопроисходит,изменим классElephant,чтобы экземпляры могли вызывать методы друг друга. Добавьте в класс Elephant метод для прослушивания сообщений. Добавим новый метод вкласс Elephant. Первыйпараметр метода содержитсообще- ние от другого объектаElephant. Во втором параметрепередается объектElephant, отправивший сообщение: public void HearMessage(string message, Elephant whoSaidIt) { Console.WriteLine(Name + " heard a message"); Console.WriteLine(whoSaidIt.Name + " said this: " + message); } Вызов метода выглядит примерно так: lloyd.HearMessage("Hi", lucinda); Мы вызываем метод HearMessage по ссылке lloyd и передаем ему два параметра: строку "Hi" и ссылку на объектLucinda. Метод использует параметр whoSaidIt для обращенияк полю Nameпереданного объектаElephant. 1 Метод SpeakTo класса Elephant использует ключе- вое слово «this» для от- правки ссылки на текущий объект другому объекту Elephant. Elep h ant Name EarSize WhoAmI HearMessage Sp ea kTo Lu cin da исп ол ьзует ссы л ку who To TalkTo (которая сейчас содержит ссылку на Lloyd) длявызоваHearMessage. this заменяется ссылкой на объект Lucinda. [ссылка на Lloyd].HearMessage("Hi , Lloyd!", [ссылка на Lucinda]); Сделайте это!
дальше 4 239 типы и ссылки При помощи ключевого слова this объект получает ссылку на самого себя. Вызовите новые методы. Добавьте еще один блок else if в метод Main, чтобы объект Lucinda отправлял сообщение объекту Lloyd: else if (input == '4') { lloyd = lucinda; lloyd.EarSize = 4321; lloyd.WhoAmI(); } else if (input == '5') { lucinda.SpeakTo(lloyd, "Hi, Lloyd!"); } else { re tur n; } Запустите программуи нажмите5.Результатдолжен выглядеть так: You pressed 5 Lloyd heard a message Lucinda said this: Hi, Lloyd! 3 Используйте отладчик и разберитесь, что здесь происходит. Установитеточкупрерыванияв команде, которая только чтобыла добавлена вметодMain: 1. Запустите программу и нажмите5. 2. Когда выполнениедостигает точкипрерывания,используйтекомандуDebug>>StepInto(F11) для пошагового выполнения с заходом в метод SpeakTo. 3. Добавьтеотслеживаниедля Name,чтобыувидеть,в какомобъектеElephantнаходится управ- ление. Внастоящее времяононаходится вобъектеLucinda, чтовполне логично, таккакметод Main вызвал Lucinda.SpeakTo. 4. Наведите указатель мыши на ключевое слово this в конце строки ираскройте его. Оно со- держитссылкуна объектLucinda: Наведитеуказатель мышина переменную whoToTalkToираскройтеее—она содержит ссылку на объектLloyd. 5. МетодSpeakTo содержит всего однукоманду—вызовwhoTalkTo.HearMessage.Зайдите вметод. 6. Управление должно находитьсявнутри метода HearMessage. Снова проверьте отслеживание — теперь полеName содержит значение «Lloyd» — объектLucinda вызвал метод HearMessage объекта Lloyd. 7. Наведите указатель мыши на переменную whoSaidIt ираскройте ее. Она содержит ссылку на объектLucinda. Завершитепошаговоевыполнениекода. Поразмышляйтенемного надтем, чтоздесьпроисходит. 4
240 глава 4 Выбор объекта из набора Есливам приходится хранить большой объем однотипных данных, таких как списки цен или клички собакизнекоторого набора, дляэтого можновоспользоваться массивом.Массивзанимает особоеместо среди типовданных,потому что онсодержитгруппу переменных, котораярассматривается как один объект. Массив предоставляетсредства для хранения иизменения несколькихфрагментов данных без отслеживания каждойпеременнойпо отдельности.Присозданиимассивобъявляетсякак обычная пере- менная,с именем итипом, — не считая того, что за типом следуют квадратныескобки. boo l[] myArray; Длясоздания массива используется ключевоеслово new. Создайтемассивиз15элементовbool: myArray = new bool[15]; Чтобызадать значение отдельногоэлементамассива, используйте квадратные скобки. Следующаякоман- да присваивает пятому элементу myArray значение true, для чего в квадратных скобкахуказывается индекс4.Индекссоответствует пятомуэлементу, потому что первыйэлемент обозначаетсяmyArray[0], второй — myArray[1] и т. д.: myArray[4] = false; Элементы массива используются как обычные переменные Чтобы использовать массив, сначала необходимо объявить ссылочную пере- менную,котораяуказывает на массив. Затем следуетсоздать объект массива командой new и указать желательный размер массива. После этого можно за- дать элементымассива. Нижеприведенпример кода,вкотором объявляется изаполняется массив,ипоказано,чтопроисходитвкучепри его выполнении. Первомуэлементу массива соответствует индекс0. // Объяв ление нового мас сива decimal // с 7 э лементами d ecimal[] prices = new decimal[7] ; pri ces[0] = 12.37M; pri ces[1] = 6_193.7 0M; // Значе ние элемента с и ндексом 2 // не за дано, он сохраня ет // значение по умолчанию 0 pri ces[3] = 1193.60 M; pri ces[4] = 58_000_ 000_000M; pri ces[5] = 72.19M; pri ces[6] = 74.8M; Массивы содержат группы значений decimal decimal decimal decimal decimal decimal decimal Строки и массивы отличаются от других типов данных, встречавшихся в этой главе, потому что только они не имеют жестко ограниченного фиксированного размера (подумайте над этим). Для создания массива используется ключевое слово new, потому что массив является объектом, — т аки м образом, переменная массива является разновидностью ссылочной переменной. В C#индексы массивов начинаются с0, т. е. первому элементу всегда соответствует индекс0. d e c i m a l a rr a y prices Пер ем ен ная pr ices является ссылочной, как и любаядругая ссылка на объект. Объект, на кото- рый она указывает, пр едст авл яет соб ой м ассив з начен ий deci mal, п ри чем все эти значения хранят- ся в одном смежном блоке кучи.
дальше 4 241 типы и ссылки О б ъ е к т D o g Массивы могут содержать ссылочные переменные Вы можетесоздать массив ссылок на объекты — по аналогии с тем, как вы создаетемассив чисел или строк. Массив не интересует, пере- менныекакого типа внем хранятся; это вашедело. Такимобразом,вы можетесоздать массивс элементами intили массив объектовDuck—ни малейших проблем небудет. Следующийкодсоздает массивизсеми элементовDog.Строка,иници- ализирующая массив,толькосоздает ссылочныепеременные.Таккак программа содержиттолько двестрокиnew Dog(),создаются только два экземпляра класса Dog. // Объявление п еременной для ма ссива // ссылок на об ъекты Dog Dog[] dogs = ne w Dog[7]; // Создаем два экземпляра Dog и помещаем // их в элементы с индексами 0 и 5 dogs[5] = new D og(); dogs[0] = new D og(); Когда вы присваиваете или читаете элемент из массива, число в квадратных скобках называется индексом. Первому элементу массива соответствует индекс 0. Все элементы в массиве являют- ся ссылками. Сам по себе массив является объектом. Dog Dog Dog Dog Dog Dog Dog Длина массива Вы можете узнать, сколько элементов содержит массив, при помощи его свойства Length. Таким образом, если имеется массив с именем prices, то для получения его длины используется вы- ражение prices.Length. Если массив содержит семь эле- ментов, вы получите значе- ние 7 — это означает, что элементы пронумерованы от0до6. Первая строка кода создала только массив, но не экземпляры. Массив представляет собой список из семи ссылок на объекты Dog — но при этом были созданы только два объекта Dog. М а с с и в Do g О б ъ е к т D o g
242 глава 4 null и void Перед вамимассив объектов Elephantицикл,которыйперебирает ихинаходит элемент снаибольшим значениемEarSize.Каким будет значение biggestEars.EarSizeпосле каждойитерациицикла for? private static void Main(string[] args) { Elephant[] elephants = new Elephant[7]; elephants[0] = new Elephant() { Name = "Lloyd", EarSize = 40 }; elephants[1] = new Elephant() { Name = "Lucinda", EarSize = 33 }; elephants[2] = new Elephant() { Name = "Larry", EarSize = 42 }; elephants[3] = new Elephant() { Name = "Lucille", EarSize = 32 }; elephants[4] = new Elephant() { Name = "Lars", EarSize = 44 }; elephants[5] = new Elephant() { Name = "Linda", EarSize = 37 }; elephants[6] = new Elephant() { Name = "Humphrey", EarSize = 45 }; Elephant biggestEars = elephants[0]; for (int i = 1; i < elephants.Length; i++) { Console.WriteLine("Iteration #" + i); if (elephants[i].EarSize > biggestEars.EarSize) { biggestEars = elephants[i]; } Console.WriteLine(biggestEars.EarSize.ToString()); } } Будьте внимательны — цикл начинает со второго элемента массива (с индексом 1) и вы- полняется шесть раз, пока «i» не достигнет длины массива. Массивы на- чинаются с индекса 0, так что первый объ- ект Elephant в массиве обозначается elephants[0]. Переменной biggestEars присваивается ссылка на объект, на который указывает elephants[i]. Итерация No 1 biggestEars.EarSize = ________ Итерация No 2biggestEars.EarSize = ________ Итерация No 3 biggestEars.EarSize = ________ Итерация No 4 biggestEars.EarSize = ________ Итерация No 5 biggestEars.EarSize = ________ Итерация No 6 biggestEars.EarSize = ________ Создаем массив c семью ссылками на Elephant. Возьми в руку карандаш
дальше 4 243 типы и ссылки null означает, что ссылка не указывает ни на что Приработес объектамичасто используется другоеважноеключевоесло- во. Когда вы создаетеновуюссылку,но ничего ейнеприсваиваете,уэтой ссылки всеравно есть значение. Висходном состоянииссылка содержит null;этоозначает, что она неуказываетнинакакойобъект. Для начала присмотримся кпроисходящемуповнимательнее: О б ъ е к т D o g No 2 БАХ! Dog fido; Dog lucky = new Dog(); fido = new Dog(); О б ъ е к т D o g No 1 lucky = null; О б ъ е к т D o g No 2 О б ъ е к т D o g No 1 fido lucky lucky fido Значение по умолчанию для любой ссылоч- ной переменной равно null. Так как fido еще не присвоено никакого зн ачени я, п ерем енная содержит null. Переменной fido п ри сваи вается ссылка на другой объект, так что она уженеравна null. Когдаlucky присваи- вается null, перемен- ная уже неуказывает на свой объект, по - этому тот помечается длясборки мусора. А null действительно пригодится мне в программе? Да. Ключевое слово null может быть очень полезным. Существует несколько типичных вариантов использования null в програм- мах. Чащевсего null используется при проверке того,что ссылка указывает на объект: if (lloyd == null) { Этапроверка вернетtrue,если ссылка lloyd содержит null. Другойвариант использованияключевогословаnull встречаетсятогда, когда вы хотите, чтобы объектбылуничтоженуборщиком мусора. Еслиу вас есть ссылка на объект и вы завершилиработу с объектом, присваивание ссылке null немедленно помечает его для сборки мусора(если только где-то не су- ществует другая ссылка).
244 глава 4 тоиэто Итерация No 1 biggestEars.EarSize = _______ Итерация No 2 biggestEars.EarSize = _______ Итерация No 3 biggestEars.EarSize = _______ Итерация No 4 biggestEars.EarSize = _______ Итерация No 5 biggestEars.EarSize = _______ Итерация No 6 biggestEars.EarSize = _______ Перед вами массив объектов Elephant и цикл, который перебирает их и на- ходит элемент с наибольшим значением EarSize. Каким будет значение biggestEars.EarSizeпосле каждойитерациицикла for? private static void Main(string[] args) { Elephant[] elephants = new Elephant[7]; elephants[0] = new Elephant() { Name = "Lloyd", EarSize = 40 }; elephants[1] = new Elephant() { Name = "Lucinda", EarSize = 33 }; elephants[2] = new Elephant() { Name = "Larry", EarSize = 42 }; elephants[3] = new Elephant() { Name = "Lucille", EarSize = 32 }; elephants[4] = new Elephant() { Name = "Lars", EarSize = 44 }; elephants[5] = new Elephant() { Name = "Linda", EarSize = 37 }; elephants[6] = new Elephant() { Name = "Humphrey", EarSize = 45 }; Elephant biggestEars = elephants[0]; for (int i = 1; i < elephants.Length; i++) { Console.WriteLine("Iteration #" + i); if (elephants[i].EarSize > biggestEars.EarSize) { biggestEars = elephants[i]; } Console.WriteLine(biggestEars.EarSize.ToString()); } } 40 42 42 44 44 45 А вы помните, что цикл начина- ется со второго элемента массива? Как вы думаете, почему? Ссылка biggestEars указывает на объект Elephant с наибольшим значением EarSize, встретившимся до настоящего момента. Восполь- зуйтесь отладчиком для проверки! Установите точку прерывания и понаблюдайте за состоянием biggestEars.EarSize. Цикл for начинается со второго объекта Elephant и сравнивает его с объектом Elephant, на который указывает biggestEars. Если у текущего объекта значение EarSize больше, то ссылка biggestEars переводится на этот объект Elephant. Далее цикл переходит к следующему элементу, затем к следующему... в итоге к концу цик- ла biggestEars будет указывать на объект Elephant с наибольшим значением EarSize. Возьми в руку карандаш Решение
дальше 4 245 типы и ссылки В: Явсееще недо конца понимаю, как работают ссылки. О:Ссылкипредоставляют механизмиспользованиявсех методов и полей объекта. Если вы создаете ссылку на объект Dog, то в дальнейшемвы сможетепользоваться этойссылкойдля обращения к любым методам, определенным для объекта Dog. Если класс Dog содержит(нестатические) методы с именамиBark иFetch, вы можетесоздать ссылку с именем spot, азатем использовать ее для вызова методов spot.Bark()и spot.Fetch().Также по ссылке можно изменять информацию, хранимую вполях объекта(таким образом, поле Breed можно изменить конструкцией spot.Breed). В: Неозначаетли это, что каждый раз, когда я изменяю зна- чение по ссылке, оно также изменяется для всех остальных ссылок, указывающих на этот объект? О:Да. Если переменная rover содержит ссылку на тот же объект, чтоиspot,топосле изменения rover.Breedна "beagle"при обращении к spot.Breed такжебудет получено значение "beagle". В:Напомните еще раз — что делает this? О: this — специальная переменная, которая может исполь- зоваться только внутри объекта. Внутри класса this может ис- пользоваться для обращения к любому полю или методу этого конкретного экземпляра. Ключевое словоthis особенно полезно при работе с классом, методы которого обращаются с вызовами к другим классам. Объект может использовать его для передачи ссылкинасамого себядругому объекту.Таким образом,еслиspot вызывает один из методов rover и передает this в параметре, объект rover получает ссылку на объект spot. В: Вы часто упоминаете сборку мусора, но кто на самом деле еюзанимается? О: Каждое приложение .NET выполняется внутри среды CLR (илиMonoRuntime, если вы запускаете свои приложенияв macOS, Linux или используете Mono вWindows). CLR делает достаточно много полезного, но есть две особенно важные операции, которые интересуют нас вданныймомент. Во-первых,CLRвыполняетваш код, аконкретно вывод, генерируемый компиляторомC#.Во-вторых, CLRуправляет памятью,используемой программой.Это означает, что CLR отслеживает все объекты, определяет, когда последняя ссылка на объект исчезает, и освобождает память, занимаемую объектом. Команда .NET в Microsoft и команда Mono в Xamarin (многолетэтобыла отдельная компания, но сейчас она является частью Microsoft) провели огромную работу, чтобы все работало быстро и эффективно. В: Я не совсемпонял, что там происходит с разными типами, вкоторых хранятся значенияразногоразмера. Можете объяснить еще раз? О: Конечно. Переменные связывают с вашим числом определен- ный размер независимо от того, насколько велико само значение. Такимобразом, если у вас имеется переменная и ей назначен тип long,тодажедля небольшогочисла(допустим,5)CLRзарезервирует достаточнопамятидля хранения максимальновозможного значения. Иеслизадуматься, это очень удобно. Вконце концов, переменные так называются именно потому, что они постоянно изменяются. CLRсчитает, чтовы знаете,чтоделаете, инебудетеназначатьпере- менной типбольше необходимого. Даже если число сейчас может быть большим, существует вероятность того, что после каких -то математических вычислений оноизменится.CLRвыделяет память, достаточную для хранения самогобольшого значения этого типа. В коде создаваемого вами объекта можно использовать специальную переменную this, которая содержит ссылку на этот объект. Задаваемые вопросы часто
246 глава 4 близкое знакомство с классом random Даже есливыне пишете код для видеоигр, из настольных игр можно многое узнать. Многиепрограммы зависятотслучайных чисел. Например,мы уже использовали класс Random для создания случайных чисел в некоторых приложениях. У большинства читателей нет прак- тического опыта использования случайных чисел... кроме игр. Бросание кубиков, тасование карт, подбрасывание монеты — все это отличные примеры генераторов случайных чисел. Класс Random выполняет функциигенератораслучайныхчиселв.NET; он используется во многих наших программах, и ваш опыт при- мененияслучайныхчиселвнастольныхиграхпоможет вам лучше понять,что онделает. Если вы никогда не играли в Go Fish, ознакомьтесь с правила- ми. Они будут использоваться позднее в этой книге! Наст оль ны е игры У настольных игр богатая история — и как выясняется, настольные игры давно влияли на развитие видеоигр, по крайней мере со времен появления первых коммерческих ролевых игр. • Первое издание «Dungeons & Dragons» (D&D) было выпущено в1974 году. И с этого же года игры, в названиях которых встречались слова «dnd» и «dungeon», стали появляться на университетских мейнфреймах. • Мы использовали класс Random для генерирования чисел. Идея использования случайных чисел в играх по- явилась очень давно — например, в настольных играх традиционно использовались кубики, карты и другие источники случайности. • В предыдущей главе было показано, что бумажный прототип может стать важным первым шагом при проекти- ровании видеоигры. Бумажные прототипы похожи на настольные игры. Собственно, бумажный прототип часто можно превратить в настольную игру и использовать ее для тестирования некоторых игровых механик. • Настольные игры, особенно карточные и классические абстрактные, могут стать хорошим учебным пособи- ем для понимания более общей концепции игровых механик. Раздача карт, тасование колоды, броски куби- ков, правила перемещения фигур на поле, использование таймера (песочных часов), правила кооперативной игры — все это примеры механик. • К механикам игры «Go Fish» относится раздача карт, требование карты удругого игрока, произнесение фразы «Go Fish» при отсутствии требуемой карты, определение победителя и т. д . Выделите минуту на чтение пра- вил: https://askwiki.ru/wiki/Go_Fish#The_game. Разработка игр... и не только
дальше 4 247 типы и ссылки Создайтеновоеконсольноеприложение—весь дальнейшийкодпойдет вметодMain. Начнитессоздания новогоэкземпляраRandom,генерированияслучайного значения int ивывода его на консоль: Random random = new Random(); int randomInt = random.Next(); Console.WriteLine(randomInt); Укажите максимальное значение для генерируемых чисел. Числа должны генериро- ваться в диапазоне от 0 до максимума (не включая).Примаксимуме10 генерируются числаот0до9: int zeroToNine = random.Next(10); Console.WriteLine(zeroToNine); Теперь смоделируйтебросаниекубика. Приминимуме1и максимуме7будутгенери- роватьсяслучайныечисла от1 до 6: int dieRoll = random.Next(1, 7); Console.WriteLine(dieRoll); Метод NextDoubleгенерируетслучайные значенияdouble.Наведите указатель мыши наимяметода, чтобы наэкране появилась подсказка, — методгенерируетчисласплавающейточкойот0.0до 1.0: double randomDouble = random.NextDouble(); Чтобы генерировать случайные числа вболее широком диапазоне,следуетумножить double на соответствующее число. Например, есливам нужны случайные значенияdouble от1 до 100,ум- ножьтеслучайное значениеdouble на100: Console.WriteLine(randomDouble * 100); Для преобразования случайных чисел double в другие типы ис- пользуйте механизм приведения типов. Попробуйте выполнить этот кодмногократно — вызаметитенезначительныеразличия взначениях floatи double. Console.WriteLine((float)randomDouble * 100F); Console.WriteLine((decimal)randomDouble * 100M); Используйте максимальное значение2длямоделирования под- брасывания монеты. В результате будет генерироваться одно издвухслучайныхчисел:0 или11.СпециальныйклассConvert содержит статический метод ToBoolean, который преобразует такойрезультатв логическое значение: int zeroOrOne = random.Next(2); bool coinFlip = Convert.ToBoolean(zeroOrOne); Console.WriteLine(coinFlip); 1 2 3 4 Тест-драйв со случайными числами Класс.NET Random ещенераз встретитсявам вэтойкниге.Чтобылучшеосвоить его, простонеобходи- мо провеститест-драйв:сесть зарульисделать парупробных кругов. ЗапуститеVisualStudio иследуйте за нами — обязательно выполнитесвой код несколько раз,потому что выбудете каждыйраз получать разные случайные числа. Как бы вы использовали класс Random для выбора случайной строки из массива строк? Мозговой штурм
248 глава 4 этот бифштекс не старый... он винтажный Добро пожаловать в забегаловку эконом-класса «У неторопливого Джо»! «У НеторопливогоДжо» подают сэндвичи. Унегоестьмясо, гора хлебаибольше приправ,чем выможетесебе представить. Но вотменюунего нет!Сможетели вы построить программу,которая генерируетслучайное новоеменю на каждый день?Да, выопределенноможете это сделать...припомощи новогоприложения WPF,массивови пары полезных приемов. Добавьте в проект новый класс MenuItem с набором полей. Взглянитена диаграмму класса. Онсодержитчетыреполя: экземплярRandom итримассива дляхраненияразличныхсоставляющих сэндвича.Поля-массивы используютинициализаторы коллекций: вы определяете элементы массива, заключая их вфигурныескобки. class MenuItem { public Random Randomizer = new Random(); public string[] Proteins = { "Roast beef", "Salami", "Turkey", "Ham", "Pastrami", "Tofu" }; public string[] Condiments = { "yellow mustard", "brown mustard", "honey mustard", "mayo", "relish", "french dressing" }; public string[] Breads = { "rye", "white", "wheat", "pumpernickel", "a roll" }; public string Description = ""; public string Price; } Добавьте метод GenerateMenuItem в класс MenuItem. ЭтотметодиспользуетужехорошознакомыйвамметодRandom.Next для выбора случайныхэле- ментовиз массивоввполяхProteins, Condiments иBreads иих конкатенациив строку: public void Generate() { string randomProtein = Proteins[Randomizer.Next(Proteins.Length)]; string randomCondiment = Condiments[Randomizer.Next(Condiments.Length)]; string randomBread = Breads[Randomizer.Next(Breads.Length)]; Description = randomProtein + " with " + randomCondiment + " on " + randomBread; decimal bucks = Randomizer.Next(2, 5); decimal cents = Randomizer.Next(1, 98); decimal price = bucks + (cents * .01M); Price = price.ToString("c"); } 1 2 Версия этого проекта для Mac доступна в приложении «Visual Studio для пользователейMac». Men uIt em Randomizer Proteins Condiments Breads Description Price Generate Этотметод вычисляет случайную цену в диапазоне от 2.01 до 5.97 преобразо- ванием двух случайных переменных int в decimal. Внимательно присмотритесь к последней строке — она возвращает price.ToString("c ") . Параметр метода ToString определяетформат. В данном случае формат "c" приказывает ToString отформатировать значение с локальной денежной единицей: в США будет выво- диться знак $, в Великобритании — ₤, в Европе — € и т. д . Сделайте это!
дальше 4 249 типы и ссылки < Grid Mar gin ="20 "> <Gri d.R owDe fini tio ns> <Ro wDef init ion /> <Ro wDef init ion /> <Ro wDef init ion /> <Ro wDef init ion /> <Ro wDef init ion /> <Ro wDef init ion /> <Ro wDef init ion /> </Gr id. RowD efin iti ons> <Gri d.C olum nDef ini tion s> <Co lumn Defi nit ion Widt h=" 5*"/ > <Co lumn Defi nit ion/ > </Gr id. Colu mnDe fin itio ns> <Tex tBl ock x:Na me= "ite m1" Fon tSiz e=" 18px " /> <Tex tBl ock x:Na me= "pri ce1" Fo ntSi ze= "18p x" H ori zont alAl ign ment ="R ight " Gr id. Colu mn=" 1"/ > <Tex tBl ock x:Na me= "ite m2" Fon tSiz e=" 18px " Gr id. Row= "1"/ > <Tex tBl ock x:Na me= "pri ce2" Fo ntSi ze= "18p x" H ori zont alAl ign ment ="R ight " Grid .Ro w="1 " Gr id. Colu mn= "1"/ > <Tex tBl ock x:Na me= "ite m3" Fon tSiz e=" 18px " Gr id. Row= "2" /> <Tex tBl ock x:Na me= "pri ce3" Fo ntSi ze= "18p x" H ori zont alAl ign ment ="R ight " Gr id. Row= "2" Grid .Co lumn ="1" /> <Tex tBl ock x:Na me= "ite m4" Fon tSiz e=" 18px " Gr id. Row= "3" /> <Tex tBl ock x:Na me= "pri ce4" Fo ntSi ze= "18p x" H ori zont alAl ign ment ="R ight " Gr id. Row= "3" Grid .Co lumn ="1" /> <Tex tBl ock x:Na me= "ite m5" Fon tSiz e=" 18px " Gr id. Row= "4" /> <Tex tBl ock x:Na me= "pri ce5" Fo ntSi ze= "18p x" H ori zont alAl ign ment ="R ight " Gr id. Row= "4" Grid .Co lumn ="1" /> <Tex tBl ock x:Na me= "ite m6" Fon tSiz e=" 18px " Gr id. Row= "5" /> <Tex tBl ock x:Na me= "pri ce6" Fo ntSi ze= "18p x" H ori zont alAl ign ment ="R ight " Gr id. Row= "5" Grid .Co lumn ="1" /> <Tex tBl ock x:Na me= "gua camo le" Fon tSi ze=" 18px " F ontS tyle ="I tali c" Grid .Row ="6 " Grid .Co lumn Span ="2 " Ho riz onta lAli gnm ent= "Rig ht" Ver tic alAl ignm ent ="Bo ttom "/> < /Gri d> Создайте XAML для формирования макета окна. Нашеприложениевыводитслучайныепункты меню вдва столбца: широкий столбец предназна- чен для названия блюда,а узкий — для цены. С каждой ячейкойв сеткесвязанэлементTextBlock, укоторого свойствуFontSize задано значение18px, кроме последней строки, которая содержит одинэлементTextBlock с выравниванием по правому краю,занимающийоба столбца. Заголовок окна имеетвысоту 350 иширину 550. Сетка снабжена отступомразмером 20. В этомпримеремырасширим раз- метку XAML, котораябыла пред- ставлена в двухпоследнихпроектах WPF. Постройте макет в конструк- торе, введите код вручную или со- вместитеэти два способа. 3 С е т к а с о с т о и т и з 7 с т р о к р а в н о й в ы с о т ы Сетка состоит из двух столбцов с шириной 5* и 1* Нижний элемент TextBlock охватывает оба столбца Сетке назначаются отступы 20, чтобы вокруг меню было немного свободного места. Присвойте элементам TextBlock в левом столбцеимена item1, item2 и т. д ., а эл емен там TextBlock в правом столбце — имена price1, price2 и т. д . Присвойте нижнему элементу TextBlock имя guacamole.
250 глава 4 случайные сэндвичи Добавьте код программной части для окна XAML. Меню генерируется методом MakeTheMenu, который вызывается вашим окном сразу же после вызова InitializeComponent. Ониспользуетмассив классовMenuItem для генерирования каждого пунктаменю.Первые три элемента должныбыть нормальными,следующиедва видасэндвичей долж- ны подаваться на нестандартных видах хлебной основы, а последний пунктявляетсяспециальным блюдом с собственным набором ингредиентов. public MainWindow() { InitializeComponent(); MakeTheMenu(); } private void MakeTheMenu() { MenuItem[] menuItems = new MenuItem[5]; string guacamolePrice; for(inti=0;i<5;i++) { menuItems[i] = new MenuItem(); if(i>=3) { menuItems[i].Breads = new string[] { "plain bagel", "onion bagel", "pumpernickel bagel", "everything bagel" }; } menuItems[i].Generate(); } item1.Text = menuItems[0].Description; price1.Text = menuItems[0].Price; item2.Text = menuItems[1].Description; price2.Text = menuItems[1].Price; item3.Text = menuItems[2].Description; price3.Text = menuItems[2].Price; item4.Text = menuItems[3].Description; price4.Text = menuItems[3].Price; item5.Text = menuItems[4].Description; price5.Text = menuItems[4].Price; MenuItem specialMenuItem = new MenuItem() { Proteins = new string[] { "Organic ham", "Mushroom patty", "Mortadella" }, Breads = new string[] { "a gluten free roll", "a wrap", "pita" }, Condiments = new string[] { "dijon mustard", "miso dressing", "au jus" } }; specialMenuItem.Generate(); item6.Text = specialMenuItem.Description; price6.Text = specialMenuItem.Price; MenuItem guacamoleMenuItem = new MenuItem(); guacamoleMenuItem.Generate(); guacamolePrice = guacamoleMenuItem.Price; guacamole.Text = "Add guacamole for " + guacamoleMenuItem.Price; } 4 Внимательно разберитесь, что здесь происхо- дит. Пункты меню 4 и 5(индексы 3 и 4) получают объектMenuItem, инициализируемый с помо- щью инициализатора объекта по аналогии с тем, как это делалось в примере с Джо и Бобом. Ини- циализация объекта присваивает полю Breads новый массив строк. Этот массив строк ис- пользует инициализатор коллекции с четырьмя строками, описывающими разные типы хлебной основы. А вы заметили, что инициализатор кол- лекции включает тип массива (new string[])? Вы не включали его при определении полей. При желании new string[] можно добавить к инициализаторам коллекций в полях MenuItem, но этого можно не делать. Эти определения не обязательны, потому что поля содержат опреде- ления типа в своих объявлениях. Последний пункт меню предназначен для специального «сэндвичадня», приготовленного из ингредиентов класса «люкс», поэтому он получает собственный объект MenuItem, у кото- рого всетри поля строковых массивов инициализируются с помощью инициа- лизаторов объектов. Использует «new string[]» для объявления типа иници- ализируемо- го массива. Включать это уточнение в поля MenuItem не нужно, по- тому что у них уже есть тип. Незабудьтевызвать метод Generate, в про- тивном случае поля MenuItem останутся пустыми, и страница будет большей частью п устой . Отдельный пункт меню, предназначенный для созда- ния новой цены нагуакамоле.
дальше 4 251 типы и ссылки Запустите программу и просмотрите сгенерированное меню. Э-э -э... Что-то не так . Все цены в новом меню одинаковы, и пункты меню выглядят странно —первые три позициисовпадают,потом следующие две, иначинаются ониодинаково.Чтопроисходит? Оказывается, класс .NET Random на самом деле является ге- нератором псевдослучайных чисел; это означает, что он по математической формуле генерирует последовательность чисел, удовлетворяющих некоторым статистическим критериям случай- ности. Такие числа достаточно хороши для любых приложений, которые строим мы свами (только не используйте их в системах безопасности, зависящих от действительно случайных чисел!).Вот почему методназывается Next — онвыдает следующеечислов по- следовательности.Формуланачинается соспециальногозначения, которое называется «затравкой», — это значение используется для вычисления следующего значения в серии. Когда вы создаете новый экземплярRandom, системные часы используются для по- лучения «затравки» формулы, новы можете задать собственное значение. Попробуйтеввестивинтерактивном окнеC# вызов new Random(12345).Next(); несколько раз. Тем самым вы приказываете создать новый экземплярRandom содним начальным значением (12345), поэтому метод Next будеткаждыйраз давать одноитоже «случайное»число. Когдавывидите,чтонесколькоразныхэкземпляров Randomдают одноито жезначение,этообъясняетсятем,что ониинициализиро- валисьпрактическив одинмоментвремени,показаниясистемных часовнеизменились, поэтомувсеонииспользовалиодно начальное значение. Как решить проблему? Используйте один экземпляр Random иобъявитеполеRandomizerстатическим,чтобывсекоман- ды MenuItem совместно использовали один экземпляр Random: public static Random Randomizer = new Random(); Сновазапустите программу — наэтот раз менюстанетслучайным. 5 Я обедаю только «У Неторопливого Джо»! Как это работает... Почему пункты меню и цены ос тают ся одинаковыми? Breads[R andomizer.Next(B reads.Length)] Breads содержит массив строк. Массив содержит пять элементов с индексами от0 до 4. Таким образом, значение Breads[0] равно "rye", а значение Breads[3] равно «a roll». Метод Randomizer.Next(7) выдает случайное значениеint, меньшее 7. Breads.Length возвращает количество элементов в массиве Breads. Таким образом, Randomizer.Next(Breads. Length) дает случайное число, большее или равное нулю, но меньшее количества элементов в массиве Breads. Если ваш компьютер достаточно производителен, возможно, ваша программа не столкнется с этой проблемой. Но если запустить ее на более медленном компьютере, вы наверняка увидите ее.
252 глава 4 глава закончена — хорошая работа ¢ Ключевое слово new возвращает ссылку на объект, которую можно сохранить в ссылочной переменной. ¢ На один объект могут указывать несколько ссылок. Объект можноизменитьпоодной ссылке, а затем об- ратиться к результатам изменения по другой ссылке. ¢ Чтобы объект оставался в куче, на него должны су- ществоватьссылки. Как только последняя ссылка на объект исчезает, объект будет уничтожен сборщиком мусора, а используемая им памятьбудетосвобождена. ¢ Программы .NET выполняются в среде CLR (Commin Language Runtime) — «прослойке» междуОС ивашей программой. Компилятор C# переводит ваш код на язык CIL (Common Intermediate Language), который выполняется CLR. ¢ Ключевое слово this позволяет объекту получить ссылку на самого себя. ¢ Массивы представляют собой объекты, содержащие несколько значений. В массивах могут храниться как значения, так и ссылки. ¢ Чтобы объявить переменную-массив, поставьте ква- дратныескобки после типа в объявлении переменной (например, bool[]trueFalseValues или Dog[] kennel). ¢ Для создания нового массива используется ключевое слово new, с указанием длины массива в квадратных скобках(например, new bool[15]или newDog[3]). ¢ Метод Length массива используется для получения его длины (например, kennel.Length). ¢ Чтобы обратиться к отдельному элементу массива, укажите его индекс в квадратных скобках(например, bool[3] или Dog[0]). Индексы массивов начина- ются с 0. ¢ null означает ссылку, которая не указывает ни на какой объект. Ключевое слово null позволяет прове- рить, равна ли ссылка null, или очистить ссылочную переменную, чтобы объект был помечен для сборки м усор а. ¢ Используйте инициализаторы коллекций дляиници- ализации массивов: при присваивании ссылки на мас- сив указывается ключевое слово new, за которым сле- дует тип массива со списком значений, разделенных запятыми, в фигурных скобках(например, new int[] {8,6,7, 5, 3, 0,9}).При инициализации пере- менной или поля в той команде, в которой они были объявлены, тип массива указывать не обязательно. ¢ МетодуToString объекта или значения можно передать параметр с форматом. Если вы вызываете метод ToString длячисловоготипа, передача значения фор- мата "c" форматирует значение с локальнойденежной единицей. ¢ Класс .NET Random представляет генератор псевдо- случайных чисел, инициализируемый показаниями си- стемных часов. Используйте один экземплярRandom для того, чтобы несколько экземпляров генератора с одинаковым исходным значением не генерировали одинаковые последовательности чисел. КЛЮЧЕВЫЕ МОМЕНТЫ
Лабораторный курс Unity No 2 https://github.com/head-first-csharp/fourth-edition Head First C# Лабораторный курсUnityNo 2 253 Unity — не только мощный кроссплатформенный движок и редактор для построения 2D- и 3D-игр и модели- рования. Также это отличный способ потрениро- ваться в написании кода C#. В предыдущей лабораторной работе вы научились ориентироваться в Unity и в трехмерном простран- стве сцены, а также начали создавать и исследовать объекты GameObject. Пришло время написать код для управления объектами GameObject. В прошлой лабора- торной работе мы ставили перед собой одну цель: на- учить вас ориентироваться в редакторе Unity (заодно эта глава позволит вам легко вспомнить материал, если вы что-то забудете). В этой лабораторной работе мы начнем писать код для управления объектами GameObject. Мы напишем код C# для исследования концепций, которые будут использоваться в других лабораторных работах Unity, начиная с метода для вращения бильярдного шара, созданного в предыдущей лабораторной работе Unity. Также мы начнем пользоваться отладчиком Visual Studio с Unity для диагностики проблем в ваших играх. Лабораторный курс Unity No 2 Написание кода C# для Unity
254 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 2 Сценарии C# добавляют поведение к объектам GameObject Итак, вы знаете, как добавить объектыGameObject в сцену. Теперь необходимо как-то заставить его ... в общем, что -нибудь делать. Unity использует сценарииC# для определения всего поведения в игре. Вэтой лабораторнойработебудут представлены инструменты,используемые приработе сC# и Unity. Мы построим простую «игру», которая на самом деле ограничивается чисто визуальным эффектом: бильярдный шар, летающий по сцене. Зайдите на UnityHub и откройте проект, созданный в первой лабораторнойработе Unity. Вэтой лабораторнойработебудетсделано следующее: Присоединение сценария C# кGameObject. С объектом GameObject типа Sphere будет связан компонент Script. При егодобавлении Unity создаст класс за вас. Мы изменим этот класс, чтобы он управлял поведе- нием бильярдногошара. Использование Visual Studio для редактирования сценария. Помните, как мы включали использова- ние VisualStudio вкачестве редакторасценариев в настройкахредактораUnity?Это означает, чтопо двой- ному щелчку на сценариив редактореUnity этот редактор откроется вVisual Studio. Воспроизведение игры в Unity. В верхней части экрана расположенакнопкаPlay.Нажатие этой кнопки запускаетвыполнение всех сценариев, связанных собъектами GameObject в сцене.Мывоспользуемсяэтой кнопкойдля выполнения сценария, добавленногок сфере. Совместное использование Unity и Visual Studio для отладки сценария. Вы уже убедились в полезно- стиотладчика Visual Studio, когдамызанималисьпоиском ошибокв коде C#.Unity идеальноинтегрируется сVisualStudio,так чтовы можете расставлятьточки прерывания, использоватьокноLocals ипользоваться другимизнакомыми средствами отладчика VisualStudioвовремя выполнения игры. 1 2 3 4 Эта лабора- торная работа продолжается с того момента, на котором за- кончилась первая, поэтому зай- дите на Unity Hub и откройте проект, создан- ный в предыду- щей лаборатор- ной ра бот е. Кнопка Play не сохраняет вашу игру! Помните, что сохраняться нужно пораньше и почаще. У многих разра- ботчиков вырабатывается привычка сохранять сцену при каждом запуске игры.
Лабораторный курс Unity No 2 Head First C# Лабораторный курсUnityNo 2 255 Добавление сценария C# к объекту GameObject Unity — нечто большее, чем просто замечательная платформа для построения2D- и 3D-игр.Многие разработчики используют Unity для художественной работы, визуализации данных, расширенной реальностии других целей. Платформа Unity особенно ценна для вас как начинающего разработчика C#,потомучто вы можете написать коддляуправлениявсем,что вы видите вигреUnity.Всеэто делает Unityзамечательным инструментом для изучения и исследования C#. Переходимк использованию C#вUnity.Убедитесь втом,что всценевыбранобъектGameObjectSphere, ищелкните на кнопке «Add Component» внижней частиокнаInspector. Unityоткрывает окнос перечнемразличных видовкомпонентов,которые можно добавить кобъекту, — иихдействительно много. ВыберитевариантNewscript, чтобыдобавитьновыйсценарийC#к объекту GameObjectSphere. Вамбудет предложено ввести имя. ПрисвойтесценариюимяBallBehaviour. Щелкнитенакнопке «Create and Add», чтобы добавить сценарий. ВокнеInspectorпоявляется компонент сименем BallBehaviour(Script). СценарийC# также появляется в окне Project. В окнеProject отображается и ерар хич еск ое представление проекта. Ваш проект Unityсостоит из файлов: аудио/видео/ графики, файлов данных, сц енар иев C#, тек стур ит.д.ВUnityэтифайлы наз ыв аются р есур сам и. В тот момент, когда вы сделали щелчок правой кнопкой для импортирования текстуры, в окне Project отображалась папка Assets, поэтомуUnity добавляет текстуру в эту п апку. А вы заметили, что сразу же после перетаскивания текстуры бильярдного шара на сферу в окне Project появилась папка с именем Materials?
256 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 2 Написание кода C# для поворота сферы В первой лабораторной работе мы приказали Unity использовать Visual Studio в качестве внешнего редактора сценариев. Сделайте двойной щелчок на новом сценарии C#. При этом Unity открывает сценарий в VisualStudio. СценарийC# содержит класс с именем BallBehaviour, содержащий два пустых метода, Start и Update: using System.Collections; using System.Collections.Generic; using UnityEngine; public class BallBehaviour : MonoBehaviour { // Start вызывается перед первым обновлением кадра void Start() { } // Update вызывается один раз на кадр void Update() { } } Авоткаквыглядитстрока кода дляповорота сферы. Добавьте ее в метод Update: transf orm.R otate (Vecto r3.up, 180 * Tim e.del taTim e); Теперь вернитесь к редактору Unity и щелкните на кнопке Play на панели инструментов, чтобы запу- с тить игр у. Ваша игразапускается, и бильярдный шар начинает вращаться с частотой дваповорота в секунду. Щелкните на кнопке P lay. Вы открыли сценарий C# в Visual Studio, щелкнув на нем в окне Hierarchy. В этом окне выводится список всех о бъ ект ов GameOb ject в текущей сцене. При создании вашего проектаUnityдобавляет сцену SampleScene с камерой и источником света. Далее вы добавили сферу, поэтому в окне Hierarchy будут отображаться все эти объекты. Если окно Hierarchy неотображается, вернитесь к макету WIde(щелкните на вкладкеGame,чтобы переключиться в представление Game). Снованажмите кнопку Play,чтобы остановить игру. При помощи кнопки Playвы сможете запустить и остановить игрув любой момент времени. Щелкните на объекте Sphere в окне Hierarchy, чтобы выделить сферу, и понаблюдайте затем, какв окне Inspector у компонента Transform изменяется угол поворота по оси Y. Если Unity не запустит Visual Studio для от- крытия сценария C#, вернитесь к началу лабора- торной работы No1 и убедитесь в том, что вы выполнили действия по настройке предпочтений в категории External Tools.
Лабораторный курс Unity No 2 Head First C# Лабораторный курсUnityNo 2 257 using System.Collections; using System.Collections.Generic; using UnityEngine; public class BallBehaviour : MonoBehaviour { // Start вызывается перед первым обновлением кадра void Start() { } // Update вызывается один раз на кадр void Update() { transform.Rotate(Vector3.up, 180 * Time.deltaTime); } } Код под увеличительным стеклом Опространствах имен было рассказано в главе 2. При создании файла со сценарием C#Unityдобавляет в начало файла строки using, чтобы иметь возможность использовать код изUnityEngineи других часто используемых пространств имен. Кадр — одна из фундаментальных концепций анимации. Unityвыводит один статический кадр, после чего очень быстро выводит следующий, и челове- ческий глаз воспринимает изменения в кадрах как движение. Unityвызывает метод Updateдля каждого объектаGameObject до того, как тот сможет дви- гаться, вращаться или вносить другие необходимые изменения. Наболее мощном компьютере частота кадров (FPS) будет выше, чем на медленном. Метод transform.Rotate заставля- ет о бъ ект GameOb ject вр ащ ать ся. В первом параметре передается ось, вокруг которой осуществля- ется поворот. В данном случае наш код использует значение Vector3.up, при котором поворот выполняется вокруг оси Y. Вто- рой параметр задает величину поворота в градусах. Наразных компьютерах игра воспроизводится с разной частотой кадров. Если онаработает с частотой 60 FPS, один поворот должен занимать 60 кадров. Если же игра работает с частотой 120FPS, один поворот должен занимать 240 кадров. Частота кадров в вашей игре даже может динамически изменяться, если в игре выполняется более или менее сложный код. В такой ситуации пригодится значение Time.deltaTime. Каждый раз, когда ядро Unity вызывает метод GameObject Update (один раз на кадр), полю Time. deltaTime присваивается доля секунды, прошедшая с момента вывода последнего кадра. Так как мы хотим, чтобы бильярдный шар совершал полный поворот каждые 2 секунды (т. е. поворот на180 градусов в секунду), для этого достаточно умножить 180 на Time. deltaTime, чтобы поворотбыл выполнен ровно на такой угол, который необходим для текущего кадра. Внутри метода Update умножение любо- го значения на Time. deltaTime преобразует его в долю этого зна- чения в секунду. Поле Time.deltaTime является статическим, и как было показано в главе 3, для его использования эк- земпляр класса Time не нужен.
258 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 2 Добавление точки прерывания и отладка игры Займемсяотладкой игрыUnity.Сначала остановитеигру, если она все еще работает(повторнымнажатием кнопки Play).Переключитесь наVisualStudio иустановитеточку прерывания в строке,добавленной в метод Update. Теперь найдите вверхнейчасти окнаVisualStudio кнопку запуска отладчика: Ì В Windows эта кнопка выглядит так: — или выберите команду меню Debug>>Start Debugging (F5). Ì В macOS эта кнопка выглядит так: — ил и выберите команду Run>>Start Debugging ( ). Щелкнитенаэтойкнопке,чтобы запустить отладчик.Снова переключитесь вредакторUnity.Есливы впервыеотлаживаетеэтотпроект, редакторUnity открываетдиалоговое окно с тремя кнопками: Выберитекнопку «Enabledebuggingforthis section» (или,есливы хотите,чтобы это окнобольше непо- являлось, выберите«Enabledebuggingforallprojects»).VisualStudio присоединяется кUnity; этоозначает, что отладчик VisualStudio теперь можетиспользоватьсядляотладки вашейигры. Нажмитекнопку Play в Unity, чтобы запустить игру.Так как среда Visual Studio присоединена к Unity, она немедленно прерывает игрув добавленной вами точке прерывания(как и в любой другой точкепре- рывания, которую бы вы могли установить). Использование счетчика попаданий для пропуска кадров Иногда бываетполезно дать вашейигренекотороевремяпоработать,преждечем она можетбыть оста- новленаточкой прерывания. Допустим,вы хотите, чтобываша играсгенерировалаи переместила врагов, преждечем внейсможет сработать точкапрерывания.Прикажемточкепрерываниясрабатывать через каждые 500 кадров. Дляэтого добавим к точке прерыванияусловие счетчика попаданий: Ì В Windows щелкнитеправой кнопкой мыши на маркере точкипрерывания( )слева отстроки, выберите в контекстном меню Conditions, после чего выберите враскрывающихся списках ва- рианты Hit Count иIs a multiple of: Ì В macOSщелкнитеправойкнопкой мышина маркереточкипрерывания( . ), выберитев меню командуEditbreakpoint...,послечеговыберитевраскрывающемсяспискевариантWhenhit count isamultipleof и введите 500 всоответствующем поле: Теперь точка прерываниябудет приостанавливать программучерез500срабатываний метода Update, или каждые500кадров. Таккакваша играработаетс частотойкадров60FPS,этоозначает,что при нажатии Continue играотработает чутьболее8 секундпередповторным прерыванием.НажмитеContinue, снова переключитесь вUnity ипонаблюдайте за вращением шара перед срабатыванием точкипрерывания. Поздравляем, вы занимае- тесь отладкой игры!
Лабораторный курс Unity No 2 Head First C# Лабораторный курсUnityNo 2 259 Сновавключите отладку игры и наведитеуказатель мыши на «Vector3. up», чтобы просмотреть значение. Выводится значение(0.0, 1.0, 0.0). Как вы думаете, что оно означает? Мозговой штурм Использование отладчика для понимания Time.deltaTime Поле Time.deltaTime будет использоваться во многих наших проектах Unity. Воспользуемся точкой прерывания иотладчиком и постараемся понять, что жеименно происходитс этим значением. Покавашаигра приостановлена на точкепрерываниявVisualStudio, наведите указатель мышина Time.deltaTime,чтобыувидеть долю секунды,прошедшую с момента вывода предыдущего кадра. За- тем добавьтеотслеживаниедля Time.deltaTime —выберитеTime.deltaTimeикомандуAddWatchиз меню,открываемого щелчком правойкнопки мыши. При каждом достижении точки прерывания в отслеживании Time.deltaTime будет выво- диться доля секунды, прошедшая с момента вывода предыдущего кадра. Как воспользоваться этим числом для вычисления текущей частоты кадров на момент создания снимка экрана? Продолжитеотладку(F5вWindows, вmacOS),чтобы возобновить игру.Шар снова начнет вра- щаться, ичерез500 кадров точка прерываниясработаетснова. Вы можетепродолжить выполнение игры по500 кадров. Обращайте вниманиена окноWatchпри каждом прерывании. Нажмите кнопку Continue, чтобы получить новое зна- чение Time.deltaTime, затем еще одно. Чтобы вычислить приблизительную частоту кадров, следует разделить 1 на Time.deltaTime. Остановите отладку (Shift+F5 в Windows, в macOS),чтобы остановить выполнениепрограммы. Затемсновазапуститеотладку.Таккакваша играпродолжает работать,точкапрерыванияпродолжит работать приповторномприсоединении VisualStudioкUnity.Завершив отладку, снова отключитеточку прерывания, чтобыона оставалась, но не вызывала прерывания припереходе. Ещераз остановите отладку,чтобы отключитьсяотUnity. Вернитесь вUnity и остановите игру — и сохраните ее, потому что кнопкаPlay не сохраняет игру автоматически. Кнопка Play в Unity за- пускает и останавли- вает игру. Среда Visual Studio остается при- соединенной к Unity, не - смотря на то что игра остановлена.
260 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 2 Добавление цилиндра для обозначения оси Y Вашасфера вращаетсявокругосиYв самом центре сцены.Добавимоченьвысокий иоченьузкий цилиндр, чтобынаглядно представитьэтуось.Создайтецилиндркомандой3D Object>>Cylinderиз менюGameObject. Убедитесь в том, что цилиндр выбран в окне Hierarchy, затем обратитесь к окну Inspector и убедитесь втом, чтоон был создан впозиции(0,0,0), а еслинет, воспользуйтесь контекстнымменю( ) иверните его вэту точку. Сделаемцилиндрвысокимиузким.ВыберитеинструментScale на панелиинструментов;либощелкните на нем , либо нажмите клавишу R. У цилиндра появляется манипулятор Scale: Манипулятор Scale очень похож на манипулятор Move, не считая того, что оси заканчиваются кубиками вместо конусов. Ваш цилиндр располагается на сфере — возможно, вы видите небольшой кусочек сферы, проступающий из середины цилиндра. Когда вы уменьшите ширину цилиндра, изменяя его масштаб по осям X и Z, сфера снова будет видна. Щелкнитеиперетащите зеленый куб,чтобырастянуть цилиндр по осиY.Затем щелкнитена красном кубе иперетащите его, чтобы сузить цилиндр по осиX; проделайтето же самое с синим кубом, чтобы сузить его по осиZ.Следите за панелью Transform в окнеInspector во время масштабирования цилинд- ра — масштаб по осиY должен увеличиться,а масштабы по осямX иZ должны стать намного меньше. ЩелкнитенаметкеXв строкеScaleна панелиTransform, перетащите еевверх-вниз .Проследитеза тем, чтобыперетаскивалась надпись X слева от текстового поля с числом. Когда вы щелкаете на метке, она окрашиваетсяв синийцвет, а вокругзначенияXпоявляется синийпрямоугольник. Приперетаски- вании указателя мыши вверх-вниз число в поле увеличивается иуменьшается, а представление Scene обновляетсяприизменениимасштаба. Будьтевнимательныприперетаскивании— масштабможетбыть как положительным, так иотрицательным. Теперь выделитечисло вполеXивведите.1 — цилиндр становится оченьузким.НажмитеклавишуTab ивведите20,затем снова нажмите Tab,введите.1 инажмитеEnter. Теперь черезсферупроходит очень длинный цилиндр, обозначающий ось Y,где Y=0.
Лабораторный курс Unity No 2 Head First C# Лабораторный курсUnityNo 2 261 Добавление полей для угла поворота и скорости Вглаве3выузнали,что классыC#могут содержать поля дляхраненияданных,которыемогут использо- ватьсяметодами.Изменимнашкодтак,чтобы внем использовались поля.Добавьтеследующиечетыре строкиподобъявлением класса,сразу жеза первойфигурнойскобкой{: public class BallBehaviour : MonoBehaviour { public float XRotation = 0; public float YRotation = 1; public float ZRotation = 0; public float DegreesPerSecond = 180; Каждое из полей XRotation, YRotation и ZRotation содержит значение от0 до 1. Набор этих чисел создает вектор,определяющийнаправление поворота: new Vector3(XRotation, YRotation, ZRotation) Поле DegreesPerSecond содержит угол поворота в градусах в секунду, который следует умножить на Time.deltaTime, какипрежде.Изменитеметод Update, чтобывнем использовались поля. Новыйкод создаетпеременнуюVector3 с именемaxis ипередает ееметоду transform.Rotate: void Update() { Vector3 axis = new Vector3(XRotation, YRotation, ZRotation); transform.Rotate(axis, DegreesPerSecond * Time.deltaTime); } Выделите объект Sphere в окне Hierarchy. Теперь поля отображаются в компоненте Script. При отобра- жении полей компонентаScript после начальных прописныхбукв добавляются пробелы, чтобы они лучшечитались. Когда вы добавляете открытые поля в класс в сценарии Unity, компонент Script выводит текстовые элементы, в которых можно редактировать эти поля. Если вы измените их, пока игра неработает, обновленные значения будут сохранены со сценой. Также значения можно редактировать во время выполнения игры, но они вернутся к исходным значениям после остановки игры. Сновазапустите игру.Покаонавыполняется, выделите объектSphere вокне Hierarchyи измените значение поля Degrees persecond на360 или90 — шар начинаетвращатьсявдвое быстрее(иливдвое медленнее). Остановитеигру— поле возвращается к значению 180. Когда игра остановится, в редактореUnity введитев поле X Rotationзначение1, а в поле Y Rotation— значение 0.Запустите игру; шарбудет вращатьсявдругомнаправлении.Щелкните нанадписиXRotation и перетащите указатель мыши вверх-вниз, чтобы изменить значение во время выполнения игры. Как только число станетотрицательным, шар начнетвращаться вобратномнаправлении. Вернитесь к по- ложительному значению,и шар вернетсяк прежнему направлению вращения. Если вы воспользуетесь редакто- ром Unity, чтобы присвоить полю Y Rotation значение 1, а затем запу- стите игру, шар будет вращаться по часовой стрелке по оси Y. Такие же поля, как те, которыедобавлялись в проекты в главах 3 и 4. Фактически это переменные, в которых хранятся значения — при каждом вызове Update одно и то же поле используется снова и снова.
262 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 2 Debug.DrawRay и 3D-векторы Вектор характеризуется длиной и направлением. Если вы уже сталкивались с векторами вучебном курсе геометрии, вероятно, вы видели диаграммы вроде следующей: Диаграмма двумерного вектора. Вектор представляется двумя числами: координатой конечной точки по оси X (4) и по оси Y(3). Обычно эти числа записываются в виде (4, 3). Y 4 3 X Но даже те изнас, кто проходилвекторына занятиях,не всегда интуитивнопонимают, какониработают, особенновтрехмерномпространстве.Это ещеоднаобласть, вкоторойC#иUnityмогут использоваться для обучения и аналитики. Использование Unity для наглядного представления векторов в трехмерном пространстве Мы добавим вигрукод,которыйпоможетвамразобратьсявтом,какработают3D-векторы. Дляначала взгляните на первую строку метода Update: Vector3 axis = new Vector3(XRotation, YRotation, ZRotation); Что эта строка сообщает о векторе? Ì У него есть тип:Vector3. Каждое объявление переменной начинается с типа. Вместо string,int или bool вектор объявляется с типомVector3.Этоттип используется вUnity для3D-векторов. Ì У него есть имя переменной: axis. Ì Для создания Vector3 используется ключевое слово new. ПриэтомполяXRotation,YRotation иZRotationиспользуются длясоздания векторов суказаннымипараметрами. Какжевыглядит3D-вектор?Небудем гадать— прощевоспользоватьсяоднимиз полезных отладочных инструментовUnity для рисованиявектора. Добавьте новую строку кода в конец методаUpdate: void Update() { Vector3 axis = new Vector3(XRotation, YRotation, ZRotation); transform.Rotate(axis, DegreesPerSecond * Time.deltaTime); D ebu g.Dr awR ay(V ect or3 .zer o, axi s, C olo r.y ello w); } Debug.DrawRay — специальныйметод,предоставляемыйUnityдляотладкиигр.Метод рисуетлуч— век- тор, проходящийотоднойточкик другой; в параметрахонполучаетначальную точку, конечную точку и цвет. Но есть одна загвоздка: луч появляется только в представлении Scene. Методы класса Debug в Unityбыли спроектированы так, чтобы они не препятствовали выполнению игры. Как правило, они влияюттолько на взаимодействие вашейигрыс редакторомUnity.
Лабораторный курс Unity No 2 Head First C# Лабораторный курсUnityNo 2 263 Запуск игры для отображения луча в представлении Scene Теперь снова запустите игру. В представлении Game вы не увидите ничего нового, потому что Debug. DrawRay— отладочныйинструмент, никакне влияющийнаигровойпроцесс. Используйте вкладкуScene, чтобы переключиться в представление Scene. Возможно,вам также придется переключиться на макет Wide,выбраввариантWideвраскрывающемсясписке Layout. Итак, вы снова вернулись к знакомому представлению Scene. Чтобы получить более полное представ- лениео том, какработают3D-векторы,выполнитеследующиедействия: Ì Используйте окноInspector дляизмененияполей сценарияBallBehaviour.Введите в поле XRotation значение0, в поле Y Rotation— значение0 и вполеZRotation — значение 3.В сцене должен по- явиться желтыйлуч,проходящий прямо по осиZ,а шардолженвращатьсявокругнего(помните: луч виден только впредставлении Scene). Вектор (0, 0 , 3) распространяется на 3 единицы по оси Z. Присмотритесь внимательнее к сетке в редакторе Unity — длина вектора составляетров- но 3 единицы. Попробуйте щелкнуть на надписиZ Rotation и перетаски- вать указатель мыши вверх-вниз в окне Inspector. При перетаскивании лучбудет увеличиваться или уменьшаться. Если значение Z вектора ста- новится отрицательным, шар начинает вращаться в другом направлении. Ì Вернитев полеZRotation значение3.ПоэкспериментируйтесперетаскиваниемзначенийXRotation иY Rotation ипосмотрите, чтоприэтом происходитс лучом. Не забывайте сбрасыватькомпонент Transform каждыйраз, когда вы их изменяете. Ì ВоспользуйтесьинструментомHand иманипуляторомScene,чтобыполучитьболееудобноепред- ставление. Щелкнитена конусеXманипулятораScene,чтобы выбратьвидсправа. Продолжайте щелкать на конусахманипулятораScene, пока неполучитевид спереди.Приэтомлегкозапутать- ся— в таком случае сбросьтемакетWide, чтобывернуться к знакомому представлению. Добавление продолжительности вывода луча Привызовеметода Debug.DrawRay можно добавить четвертый аргумент со значением времени, в тече- ниекоторого луч долженоставаться на экране. Добавьте аргумент .5f,чтобы каждый луч оставался на экране полсекунды: Debug.DrawRay(Vector3.zero, axis, Color.yellow, .5f); Сновазапуститеигруипереключитесь впредставлениеScene. Теперь приперетаскиваниичиселвверх- вниз на экранебудет оставаться след из лучей. Эффектвыглядит весьма интересно, но что еще важнее, онявляетсяхорошим средствомвизуализации3D-векторов. След, остающийся за лучом, по- могает создать интуитивное представление о том, как рабо- тают 3D-векторы.
264 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 2 Поворот шара вокруг точки сцены Нашкод вызывает метод transform.Rotate, чтобы шар вращался относительно его центра. Выделите объектSphereв окнеHierarchy и введитезначение5 в полеXPosition компонентаTransform.Затем воспользуйтесь контекстным меню ( )компонентаScriptBallBehaviour для сбросаполей. Снова запу- стите игру— теперь шарбудет находитьсявпозиции(5,0,0)ибудет вращатьсяотносительно своейосиY. При изменении значения X Position на 5 шар начинает вращаться в точке, смещенной относительно центра сцены. Изменим метод Update, чтобы в нем использовалась другаяразновидность вращения. Заставим шар вращаться относительно центральнойточкисцены — точкис координатами(0,0,0), — используяметод transform.RotateAround, который поворачивает объектGameObject относительно заданной точки сцены. (Онотличается отметода transform.Rotate, который использовался ранее и поворачивал объ- ект GameObject вокругсвоего центра.)В первом параметре передается точка, относительно которой осуществляется вращение. Мы будем передавать в этом параметреVector3.zero, который является со- кращением длязаписи new Vector3(0, 0, 0). Новый метод Update выглядит так: void Update() { Vector3 axis = new Vector3(XRotation, YRotation, ZRotation); tr ans form .Ro tat eAro und (Ve ctor 3.z ero , ax is, Deg ree sPe rSec ond * Time .de lta Time ); Debug.DrawRay(Vector3.zero , axis, Color.yellow, . 5f); } Теперь запуститесвой код. В новой версии шар описываетбольшие кругиотносительно центральной точ ки: Новый метод Update поворачивает шар вокруг точки (0, 0, 0) сцены.
Лабораторный курс Unity No 2 Head First C# Лабораторный курсUnityNo 2 265 Эксперименты с поворотами и векторами в Unity Мыбудем работать с 3D-объектами и сценами в остальных лабораторныхработахUnity в этой книге. Дажетеизнас,кто проводит много времени за3D-играми,неимеют абсолютно четкого представления о том,как работаютвекторы и3D-объекты икак выполняютсяперемещения иповоротыв трехмерном пространстве. Ксчастью, Unityявляетсяотличным инструментом дляисследования того, как работают 3D-объекты. Давайтенемного поэкспериментируем. Покавашкодработает,попробуйтеизменить параметры,чтобыпоэкспериментировать с поворотами: Ì Вернитесь к представлениюScene,чтобы на экране появился желтый луч, который отобража- ется методом Debug.DrawRay в методе BallBehaviour.Update. Ì Выделите объект Sphere в окнеHierarchy. Перечень компонентов должен появиться в окне Inspector. Ì Введитезначение10вполяхXRotation, YRotationиZRotationкомпонентаScript, чтобывектор отображалсяввиде длинноголуча.Используйте инструментHand(Q)дляповорота представления Scene,пока луч небудетхорошо виден. Ì Используйте контекстное меню компонента Transform ( ) для сброса компонента Transform. Таккак центр сферы находится вначале координат сцены (0,0,0),сфера будет поворачиваться относительно своего центра. Ì Введите в поле XPositionкомпонентаTransform значение 2.Шар должен вращатьсяотносительно вектора. Выувидите,чтошар,пролетающийрядом сцилиндромосиY,отбрасываетнанеготень. Пока игра работает, введите в полях X, Y и Z Rotation компонента Script BallBehaviour зна- чение 10, сбросьте состояние компонента Transform сферы и введите в его поле X Position значение 2 — как только это будет сделано, сфера начнет вращаться вокруг луча. Попробуйтеповторитьтрипоследнихшага дляразныхзначе- нийX,Y иZ, каждыйразсбрасываякомпонентTransform, чтобы начать с фиксированной точки. Затем попробуйте пощелкать на надписях полей Rotation и перетащить их вверх-вниз — это поможетвам лучшепонять,как работают повороты. Unity — отличный инструмент для ис- следования работы 3D-объектов, позво- ляющий изменять свойства объектов GameObject в реаль- ном времени.
266 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 2 ¢ Манипулятор Scene всегда отображает ориентацию камеры. ¢ К любому объекту GameObject можно присоединить любой объект GameObject.Метод Update сценария вы- зывается одинраз для каждого кадра. ¢ Метод transform.Rotate заставляет объект GameObject повернуться на заданныйугол вградусах позаданной оси. ¢ В методе Update умножение любого значения на Time. deltaTime преобразует это значение в расчете на се- кунду. ¢ Отладчик Visual Studio можно присоединить к Unity, чтобы проводить отладку игры во время ее выполне- ния. Отладчик остается присоединенным к Unity даже в том случае, если игра не выполняется. ¢ При добавлении условия счетчика попаданий точ- ка прерывания будет срабатывать после выполнения соответствующей команды некоторое количество раз. ¢ Поле представляет собой переменную, которая суще- ствует внутри класса за пределами его методов. Значе- ния полей сохраняются между вызовами метода. ¢ Если вы включите открытые поля в класс в сценарии Unity, компонент Script отображает текстовые элемен- ты для изменения этих полей. Между прописными буквами вставляются пробелы, чтобы имена лучше читались. ¢ 3D-векторы могут создаваться вызовом new Vector3. (Ключевое слово new рассматривалось в главе3.) ¢ МетодDebug.DrawRay рисует вектор в представлении Scene(но не в представленииGame). Векторы могут ис- пользоваться не только для отладки, но и как учебный инструмент. ¢ Метод transform.RotateAround поворачивает объект GameObject вокруг заданной точки сцены. КЛЮЧЕВЫЕ МОМЕНТЫ Проявите фантазию! Ваша очередь для самостоятельных экспериментов сC# и Unity.Выуже видели, как C# интегрируется с объектами GameObject в Unity. Выделите немного времени и поэкспериментируйтес различными инструментамии методами Unity, о которых выузналив первыхдвух лабораторныхработах. Пр ивед ем неск ол ь ко идей: Ì Добавьте в сцену кубы, цилиндры или капсулы. Присоедините к ним новые сценарии(проследите за тем,чтобыкаждомусценариюбылопри- своеноуникальное имя!)изаставьтеихвращаться вразных направлениях. Ì Попробуйтеразместить вращающиесяобъектыGameObjectвразных позициях сцены. Удастся ли вам создать интересные визуальные эф- фектыизнесколькихвращающихся объектовGameObject? Ì Добавьтексценеисточниксвета. Что произойдет,если использовать метод transform.rotateAround для поворота нового источника света по различным осям? Ì Небольшая задача изобластипрограммирования: попробуйте исполь- зовать оператор +=длядобавлениязначения кодномуиз полейсцена- рия BallBehaviour. Не забудьте умножить значение на Time.deltaTime. Попробуйтедобавить командуif,которая сбрасываетполедо0,если его значениестало слишком большим. Поэкспери- ментируйте с только что изученны- ми приемами и инструмен- тами. Это от- личный способ использования Unity и Visual Studio в целях обучения и ис- следования. Прежде чем запускать код, определите, что он сделает. Работает ли код так, как положено? Прогнозирование поведения нового кода — отличный прием для повышения квалификации C#.
Т А Й Н Ы Й д н е в н и к Б и л л и НЕ ОТКРЫВАТЬ П Р И В А Т Н О ТОЛЬКО ДЛЯ БИЛЛИ N Инкапсуляция 5 Умейте хранить секреты Вам когда-нибудь хотелось, чтобы посторонние не лезли в ваши личные дела? Вот и вашим объектам этого иногда хочется. И если вы не желаете, чтобы чужие люди читали ваш дневник или просматривали банковские выписки, хорошие объекты не позволяют другим объектам копаться в их полях. В этой главе выузнаете о мощи инкап- суляции — приеме программирования, который делает вашкод более гибким. Такой код трудно использовать некорректно. Данные вашего объекта объявляются приватными, и к ним добавляются свойства, защищающие обращения к этим данным. А это нормально, что мы читаем дневникБилли? Конечно! Иначе он бы оставил нам какую-нибудь ПОДСКАЗКУ, что он этого не хочет.
268 глава 5 Оуэну снова нужна помощь class SwordDamage { public const int BASE_DAMAGE = 3; public const int FLAME_DAMAGE = 2; public int Roll; public decimal MagicMultiplier = 1M; public int FlamingDamage = 0; public int Damage; public void CalculateDamage() { Damage = (int)(Roll * MagicMultiplier) + BASE_DAMAGE + FlamingDamage; } public void SetMagic(bool isMagic) { if (isMagic) { MagicMultiplier = 1.75M; } el se { MagicMultiplier = 1M; } CalculateDamage(); } public void SetFlaming(bool isFlaming) { CalculateDamage(); if (isFlaming) { Damage += FLAME _DAMAGE; } } } Поможем Оуэну реализовать броски на повреждения Оуэну настолько понравился калькулятор характеристик, что онзахотел создать другиепрограммыC#,которые он мог бы использовать всвоихиграх.В игре,которуюонве- дет внастоящеевремя,прилюбойатакемечомбросаются кубики ипоформулевычисляются поврежденияотатаки. Оуэн записал формулу вычисления повреждений от удара мечом всвоем блокноте гейм-мастера. НижеприведенклассSwordDamage,которыйвыполняет вычисления. Внимательнопрочитайте код—вампредстоит написать приложение, вкоторомонбудет использоваться. Ещеодна полезная возможность C#. Так как базовые повреждения и повреждения от огня в программе не будут изменяться, их можно объ- явить как константы с ключевым словом const. Константы в целом похожи на переменные, но их значение никогда не изменяется. Если вы напи- шете код, который пытается изменить константу, компилятор выдаст сообщение об ошибке. Теперь я могу проводить меньше времени за вычислениями и больше времени посвятить тому, чтобы сделать игру более интересной для игроков. Формула для вычисления повреждений. Выделите немно- го времени и прочитайте код, чтобы понять, как он реализует эту формулу. Так как огненные мечи наносят еще больше повреждений в дополнение к броску, метод SetFlaming вычис- ляет повреждения и прибавляет к ним значение FLAME_DAMAGE. * чтобы определить количеСтво повреждений (HP) от ата- ки мечом, броСьте 3d6 (три шеСтигранных кубика) и до- бавьте «базовые повреждения» 3 HP. * еСли при атаке иСпользуетСя огненный меч, атака причиня- ет дополнительные 2 HP по- вр еж ден ия. * на некоторые мечи наложены магичеСкие заклятия. для волшебных мечей броСок 3d6 умножаетСя на 1.75 и округ­ ляетСя в нижнюю Сторону, по- Сле чего к нему прибавляютСя базовые повреждения и по- вреждения от огня. Описание формулы вычисления повреж- дений из б локн ота гейм- мастера Оуэ на.
дальше 4 269 инкапсуляция Создание консольного приложения для вычисления повреждений Построимконсольное приложение, которымОуэнбудет пользоватьсядляработыс классомSwordDamage. Приложениевыводит на консоль сообщение, котороепредлагаетуказать,являетсялимеч волшебным и/или огненным,а затем выполняетвычисления. Пример вывода приложения: 0 for no magic/flaming, 1 for magic, 2 for flaming, 3 for both, anything else to quit: 0 Rolled 11 for 14 HP 0 for no magic/flaming, 1 for magic, 2 for flaming, 3 for both, anything else to quit: 0 Rolled 15 for 18 HP 0 for no magic/flaming, 1 for magic, 2 for flaming, 3 for both, anything else to quit: 1 Rolled 11 for 22 HP 0 for no magic/flaming, 1 for magic, 2 for flaming, 3 for both, anything else to quit: 1 Rolled 8 for 17 HP 0 for no magic/flaming, 1 for magic, 2 for flaming, 3 for both, anything else to quit: 2 Rolled 10 for 15 HP 0 for no magic/flaming, 1 for magic, 2 for flaming, 3 for both, anything else to quit: 3 Rolled 17 for 34 HP 0 for no magic/flaming, 1 for magic, 2 for flaming, 3 for both, anything else to quit: q Press any key to continue... Нарисуйте диаграмму класса SwordDamage. Затем создайте новое консольное приложение и до- бавьтекласс SwordDamage. В процессе ввода внимательно проследите за тем, как работают методы SetMagic и SetFlaming и чем они отличаются другот друга. Когда вы будетеуверены в том, что пони- маете их логику, можно переходить к построению методаMain. Он будет действовать по следующей схеме: 1. Создать новый экземпляр класса SwordDiagram, а также новый экземпляр Random. 2. Вывести на консоль запрос и определить нажатие клавиши. Вызвать метод Console.ReadKey(false), чтобы введен- ная пользователем клавиша была выведена на консоль. Если нажатая клавиша отлична от 0,1 ,2 или 3, выполнить командуreturn для завершения программы. 3. Смоделировать бросок 3d6. Для этого вызвать random.Next(1, 7) три раза, сложить результаты и присвоить значе- ние полю Roll. 4. Если пользователь нажал 1 или 3, вызвать SetMagic(true); в противном случае вызвать SetMagic(false). Команда if для этого не нужна: key == '1' возвращает true, так что вы можете использовать || для проверки нажатой клави- ши прямо внутри аргумента. 5. Если пользователь нажал 2 или 3, вызвать SetFlaming(true); в противном случае вызвать SetMagic(false). И снова это можно сделать в одной команде с использованием == и ||. 6. Вывестирезультаты на консоль. Внимательно просмотритерезультат и используйте \nдля вставки разрывов строк там, где они нужны. Бросок 11 для неволшебного и неогненного меча наносит повреждения в размере 11+3=14 HP. Бросок 11 для волшебного меча наносит повреждения вразмере(11×1.75=19)+3=22HP. Бросок 17 для волшебного огненного меча наносит по- вреждениявразмере(17 × 1.75=29) + 3 + 2 = 34 HP. Упражнение
270 глава 5 пока неплохо Консольное приложение создает новый экземпляр класса SwordDamage, который мы предоставили (и экземпляр Randomдля моделирования бро- сков 3d6), и выдает результат, соответствующий приведенному примеру. public static void Main(string[] args) { Random random = new Random(); SwordDamage swordDamage = new SwordDamage(); while (true) { Console.Write("0 for no magic/flaming, 1 for magic, 2 for flaming, " + "3 for both, anything else to quit: "); char key = Console.ReadKey().KeyChar; if (key != '0' && key != '1' && key != '2' && key != '3') return; int roll = random.Next(1, 7) + random.Next(1, 7) + random.Next(1, 7); swordDamage.Roll = roll; swordDamage.SetMagic(key == '1' || key == '3'); swordDamage.SetFlaming(key == '2' || key == '3'); Console.WriteLine("\nRolled " + roll + " for " + swordDamage.Damage + " HP\n"); } } Превосходно! Но я хотел спросить... Нельзя ли создать более наглядное приложение? Да! Мы можем построить приложение WPF, которое использует тот же класс. Попробуем повторно использовать классSwordDamage вприложенииWPF.Пер- вая проблема, с которой мы сталкиваемся, — создание интуитивного повторного использования.Меч можетбытьволшебным,огненным,итем идругимилинитем нидругим; нужно как-то реализовать всеэтиварианты вграфическом интерфейсе, а вариантов много. Можно создать набор переключателей илираскрывающийся список с четырьмя вариантами — по аналогии с четырьмя вариантами, которые предоставляет консольное приложение. Тем не менее мы решили, что более на- глядным будет решениесфлажками. ВWPF класс флажка CheckBox использует свойство Content для вывода надписи справа от элемента подобно тому, как Button использует свойство Content для вы- водимого текста. ИмеютсяметодыSetMagic иSetFlaming, имыможемиспользовать события Checked и Unchecked элемента CheckBox для задания методов, которые должны вызываться приустановкеилиснятиифлажка пользователем. SwordDamage Roll MagicMultiplier FlamingDamage Damage CalculateDamage SetMagic SetFlaming Версия этого проекта для Mac доступна в приложении «Visual Studio для пользователей Mac». Упражнение Решение
дальше 4 271 инкапсуляция Разработка XAML для WPF-версии калькулятора повреждений СоздайтеновоеприложениеWPF,задайтетекст заголовка главного окнаSwordDamage,выберитевы- соту175иширину300.Включите всеткутристрокиидва столбца. Вверхнейстроке должнынаходиться два элемента CheckBox с надписями Flaming иMagic, всредней — элемент Button с надписью «Rollfor Damage», занимающий оба столбца,и внижней— элементTextBlock,такжезанимающийоба столбца. Нижеприведен код XAML — конечно, для построения формы можновоспользоватьсяконструктором, но прижеланиивы также можете ввести XAML вручную. <Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <CheckBox x:Name="flaming" Content="Flaming" HorizontalAlignment="Center" VerticalAlignment="Center" Checked="Flaming_Checked" Unchecked="Flaming_Unchecked"/> <CheckBox x:Name="magic" Content="Magic" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center" Checked="Magic_Checked" Unchecked="Magic_Unchecked" /> <Button Grid.Row="1" Grid.ColumnSpan="2" Margin="20,10" Content="Roll for damage" Click="Button_Click"/> <TextBlock x:Name="damage" Grid.Row="2" Grid.ColumnSpan="2" Text="damage" VerticalAlignment="Center" HorizontalAlignment="Center"/> </Grid> . . . Выделите элемент CheckBox, затем восполь - зуйтесь кнопкой Events в окне Properties для отображения событий. После ввода имени элемента в верхней части окна вы може- те сделать двойной щелчок наэлементах Checked и Unchecked — IDEдобавит их авто- матически и использует имена элементов для генерирования имен методов — обработчи- ков событий. Этот текст будет заменен выводом («Rolled 17 for 34HP»). Обработчики со- бытий Checked и Unchecked вызыва- ются при установке или снятии флажков. Присвойте элементам CheckBox имена magic и flaming, а элементу TextBlock — имя damage. Убедитесь в том, что имена правильно указаны в свой- ствах x:Name в коде XAML. Сделайте эт о!
272 глава 5 Код программной части для WPF-калькулятора повреждений Добавьтеэтоткод программнойчастив свое приложениеWPF.Он создает экземп- лярыSwordDamage иRandom, а также включает флажки и кнопку в вычисление повреждений: public partial class MainWindow : Window { Random random = new Random(); SwordDamage swordDamage = new SwordDamage(); public MainWindow() { InitializeComponent(); swordDamage.SetMagic(false); swordDamage.SetFlaming(false); RollDice(); } public void RollDice() { swordDamage.Roll = random.Next(1, 7) + random.Next(1, 7) + random.Next(1, 7); DisplayDamage(); } void DisplayDamage() { damage.Text = "Rolled " + swordDamage.Roll + " for " + swordDamage.Damage + " HP"; } private void Button_Click(object sender, RoutedEventArgs e) { RollDice(); } private void Flaming_Checked(object sender, RoutedEventArgs e) { swordDamage.SetFlaming(true); DisplayDamage(); } private void Flaming_Unchecked(object sender, RoutedEventArgs e) { swordDamage.SetFlaming(false); DisplayDamage(); } private void Magic_Checked(object sender, RoutedEventArgs e) { swordDamage.SetMagic(true); DisplayDamage(); } private void Magic_Unchecked(object sender, RoutedEventArgs e) { swordDamage.SetMagic(false); DisplayDamage(); } } Вы уже видели, что код конкретной программы можно написать множеством разных способов. Возможно, во многих проектах этой книги можно было бы найти другой — не менее эффек- тивный — способ решения проблемы. Но для калькулятора повреждений Оуэна введите код точно в таком виде, в каком он приведен здесь, потому что (внимание, спойлер!) мы наме- ренно включили в него несколько ошибок. Готовый код Очень внимательно прочитайте этот код. Сможете ли вы найти какие-то ошибки до его выполнения? хм-м... что-то не так Сделайте это !
дальше 4 273 инкапсуляция Джейден: Оуэн, о чем ты говоришь? Оуэн: Я говорю о новом приложении, которое вычисляет повреждения от ударов мечом... автоматически. Мэтью: Потому что бросать кубики очень, очень утомительно. Джейден: Да ладно,зачем столько сарказма. Давайтепопробуем. Оуэн:Спасибо,Джейден.И момент самыйподходящий, потомучтоБриттанитолько что ударила корову-оборотнясвоим огненным волшебным мечом. Давай,Бриттани, попробуй. Бриттани: Хорошо. Мы только что запустили приложение, я установила флажок Magic.Похоже, здеськакой-тонеправильныйбросок, давайтеяснова нажмукнопку, и... Джейден:Нет, что-то не так.Ты выбросила14, ноприложениевсеравно выдает всего 3HP.Нажми ещераз. Выпало11,и снова 3 HP.Пробуем ещераз. 9,10,5 — и каждый раз3HP.Оуэн, вчем дело? Бриттани:Вообще-то приложение вкакой-тостепениработает. Еслисмоделировать бросок, а потом паруразустановить флажки, со временем оно начинает выдавать правильныйответ. Смотрите, явыбросила10с повреждениями22HP. Джейден: Ты права. Причемделать всеследует встрогоопределенном порядке.Сначаланажать кнопку броска, потом установить нужныефлажки иобязательноустановитьфлажокFlaming дважды. Оуэн: Верно. И еслипроделать всеименно втаком порядке,программа работает. Но если где-то нару- шить этотпорядок,она ломается. Ладно,можно итак. Мэтью:Или... Может,просто делать всепо-старому, с настоящимикубиками? Разговор за столом (или, может, дискуссия о кубиках?) Наступаетночь игры!Вся группаОуэна в сборе, ион собирается пустить в ход свой новейший кальку- лятор повреждений.Посмотрим,что происходит. Хорошо, коллеги, у нас новое правило. Приготовьтесь: вы будете в шоке от невероятных чудес современной технологии. Бриттани и Джейден пра- вы. Программа работает, но только если делать все в строго определенном порядке. Вот как приложение выглядит при запуске. Но если щелкнуть на флажке Flaming дважды, число будет правильным. Попробуем вычислить по- вреждения для удара огненным волшебным мечом — сначала установим флажок Flaming, а потом флажок Magic. Стоп — неправильный результат.
274 глава 5 думайте, прежде чем исправлять Попробуем исправить ошибку Когда вызапускаете программу, чтоонаделает в первуюочередь?Повнимательнееприсмот- римсякэтомуметодувсамомначале классаMainWindowскодомпрограммной части окна: public partial class MainWindow : Window { Random random = new Random(); SwordDamage swordDamage = new SwordDamage(); public MainWindow() { InitializeComponent(); swordDamage.SetMagic(false); swordDamage.SetFlaming(false); RollDice(); } Еслиукласса имеетсяконструктор, то присозданииновогоэкземпляра этогокласса конструктор станет первым выполняемымкодом.Когда приложениезапускается исоздает экземплярMainWindow,сначала оно инициализируетполя (включая создание объектаSwordDamage), а затем вызывает конструктор. Такимобразом,программа вызываетRollDice непосредственно перед отображениемокна, ипроблема встречаетсяприкаждомнажатиикнопкиброска — следовательно, не исключено, что решение проблемы можетбытьреализовано вметодеRollDice. Внеситеследующие изменения в метод RollDice: public void RollDice() { swordDamage.Roll = random.Next(1, 7) + random.Next(1, 7) + random.Next(1, 7); swordDamage.SetFlaming(flaming.IsChecked.Value); swordDamage.SetMagic(magic.IsChecked.Value); DisplayDamage(); } Протестируйтесвой код. Запустите программуи несколькораз щелкните на кнопке. Пока все хорошо— числа выглядят нормально. ТеперьустановитефлажокMagicи щелкните на кнопке еще несколько раз. Отлично, исправлениесработало!Осталось протестироватьпоследнюю мелочь.УстановитефлажокFlaming, щелкните на кнопке и... Ой!Снова неработает. Когдавы щелкаете на кнопке, множитель 1.75для волшеб- ного меча применяется, но дополнительные 3 HP для огненныхмечей не прибавляются. Для получения правильного числа всеравно нужноустановить и снятьфлажокFlaming.Программа все еще не работает. Мы выдвинули предположение и быстро написали код, но он не решил проблему, потому что мы не размышляли над тем, что же было причиной ошибки. Всегда думайте над тем, из-за чего произошла ошибка, прежде чем пытаться исправить ее. Когда в вашем коде что-то идет не так, очень соблазнительно с ходу взяться за дело и немедленно начать писать еще больше кода для исправления. Может по- казаться, что вы быстро реагируетена проблемы, но в таких ситуациях слишком легко добавить новую порцию ошибочного кода. Всегда надежнее не торопиться и разобраться, что же действительно вызвало ошибку, вместо того чтобы просто попытаться слепить решениена скоруюруку. Исправьте! Вызов IsChecked.Value для флажка возвращает true, если флажок установ- лен, или false для снятого флажка. Этот метод называется конструктором. Он вызывается при создании экземпляра класса MainWindow и может использо- ваться для инициализации экземпляра. Конструктор не имеет возвращаемого типа, а его имя совпадает с именем класса.
дальше 4 275 инкапсуляция Использование Debug.WriteLine для вывода диагностической информации Внесколькихпоследнихглавахмыпользовались отладчиком длявыявленияошибок, ноэто не единствен- ный инструмент, применяемыйразработчиками для поиска ошибок в коде. Когда профессиональные разработчики занимаются диагностикой ошибок, одной из самых частых мер становится добавление команд диагностического вывода. Именно это мы сделаем длявыявления ошибки. Строковая интерполяция Вы уже использовали оператор + для конкатенации строк. Это весьма мощный инструмент — вы можете взять любое значение (отличное от null), и оно будет безопасно преобразовано в строку (обычно вызовом метода ToString). Проблема в том, что конкатенация нередко услож- няет чтение кода. К счастью, C# предоставляет отличное средство для упрощения конкатенации строк. Этот механизм называется строковой интерполяцией, и чтобы воспользоваться им, достаточно поставить знак $ перед строкой. Чтобы включить в строку переменную, поле или сложное выражение (даже вызов метода!), заключите их в фигурные скобки. Если в строке должны при- сутствовать литеральные фигурные скобки, удвойте их: {{ }}. ОткройтеокноOutputвVisualStudio командойOutputизменю View(Ctrl+OW).Любойтекст, который выводится вызовом Console.WriteLine из приложения WPF, отображается в этом окне. Используйте Console.WriteLine только для вывода текста, который должен быть виден вашим пользователям. Если же строкивыводятсяисключительнодляотладочных целей,следует использоватьDebug.WriteLine.Класс Debug находитсяв пространствеименSystem.Diagnostics,поэтому начните с добавлениястрокиusing вначало файла классаSwordDamage: using System.Diagnostics; Затем добавьте команду Debug.WriteLine в конец метода CalculateDamage: public void CalculateDamage() { Damage = (int)(Roll * MagicMultiplier) + BASE_DAMAGE + FlamingDamage; Debug.WriteLine($"CalculateDamage finished: {Damage} (roll: {Roll})"); } ВключитеещеоднукомандуDebug.WriteLine в конецметодаSetMagicи еще однув конец метода SetFlaming. Этикоманды должны быть идентичны добавленной вCalculateDamage, кроме того, что они выводят «SetMagic» и «SetFlaming» вместо «CalculateDamage»: public void SetMagic(bool isMagic) { // остальные команды метода SetMagic без изменений Debug.WriteLine($"SetMagic finished: {Damage} (roll: {Roll})"); } public void SetFlaming(bool isFlaming) { // остальные команды метода SetFlaming без изменений Debug.WriteLine($"SetFlaming finished: {Damage} (roll: {Roll})"); } Т еперь ваша пр о- грамма будетвы- водить полезную диагностическую информацию в окне Output.
276 глава 5 отладкабез отладчика Воспользуемся окном Output для отладки приложения. Запустите программу и понаблюдайте за окном Output.Во время загрузки выувидите ряд сообщений, в которых говорится, что CLR загрузиларазличныеDLL(это нормально, не обращай- те внимания). Когда на экране появится главное окно, нажмите кнопку Clear All ( ), чтобы очистить окно Output. Установите флажок Flaming. Следующий снимок экрана был сделан длярезультата броска 9: 14 — правильный ответ;9 + базовое повреждение 3 + 2для огненного меча. Пока все нормально. Из окна Output видно, что произошло: метод SetFlaming сначала вызвал метод CalculateDamage, который вычислил ре- зультат 12. Затем было добавлено значение FLAME_DAMAGE, что дало результат 14, и наконец, была выполнена до- бавленная вами команда Debug.WriteLine. Нажмите кнопку, чтобы повторить бросок. Программа должна вывести еще три строки в окнеOutput: Накубиках выпало12, вычисленный результатдолжен бытьравен17.Что же отладочный вывод говорит о произошедшем? Сначала был вызван метод SetFlaming, который присвоил Damage значение 17, — и эт о правильно: 12 + 3 (базовые по- вреждения) + 2(огненные повреждения). Но затем программа вызвала метод CalculateDamage, который заменил значение в полеDamage и вернул значение 15. Проблема в том, что метод SetFlaming был вызван до CalculateDamage, и несмотря на то что огненные повреждения были добавлены правильно, последующий вызов CalculateDamage отменил их. Итак, настоящая причина того, что про- грамма не работает, заключается в том , что поля и методы класса SwordDamage должны использоваться в строго определенном порядке: 1. Присвоить полю Roll результат броска 3d6. 2. Вызвать метод SetMagic. 3. Вызвать метод SetFlaming. 4. Не вызывать метод CalculateDamage, потому что SetFlaming делает это за вас. Вот почему консольное приложение работало, а приложение WPF не работает. Консольное приложение использовало класс SwordDamage конкретным способом, при котором он работает. Приложение WPF вызвало методы в неправильном порядке и по- этому получило неправильные результаты. По с леду Ага! Теперь мы действительно знаем, почему программа не работает. Debug.WriteLine — один из основных(и са- мых полезных!) отладочных инструментов в инструментарии разработчика. Иногда самый быстрый способ выявления ошибок в коде заключается в стратегическом разме- щении команд Debug.WriteLine для получе- ния важной информации, которая поможет в решении проблемы. Эту ошибку можно выявить и без установки точек прерывания. Разработчики поступают так очень ча- сто... а значит, вам тоже стоит этому научиться!
дальше 4 277 инкапсуляция Значит, методы просто должны вызываться в определенном порядке. И в чем проблема? Меняем порядок их вызова, и программа начинает работать. Люди не всегда используют ваши классы в точности так, как вы ожидаете. И чаще всего этими классами будете пользоваться вы сами! Сегодня вы пишете класс,которыйбудет использоваться завтра илив следующем месяце. К счастью, C# предоставляет мощный механизм,которыйповышает вероятность того, что программавсегдабудетработать правильно, даже если пользователи делают что-то такое,что вамвголовунеприходило.Этот механизм называетсяинкапсуляцией, ион чрезвычайнополезен приработе с объектами. Целью инкапсуляцииявляется ограничениедоступа к«внутренностям» вашихклассов,чтобы всеполяиметоды классовбылибезопаснымивиспользовании, а возможностиихнекорректного использования были сведены к минимуму. Инкапсуляция позволяет создавать классы, приработе с которыми намного труднее совершить ошибку, — и это от­ личный способ предотвращения ошибок вроде той,которую мыобнаружили вкалькулятореповреждений. В: Чем отличаются методы Console.WriteLine и Debug. Wr it eL ine? О:Класс Console используетсяконсольными приложениямидля получения входных данных и передачи результатов пользовате- лю. Он использует три стандартных потока, предоставляемых операционной системой: стандартный ввод (stdin), стандартный вывод(stdout) и стандартный поток ошибок (stderr). Стандартный ввод — текст , который передается программе, а стандартный вывод — текст , который программа выводит. (Есливы когда-либо использовали перенаправление ввода/вывода в командной обо- лочке или командной строке с использованием <, > , |, <<, >> либо ||, считайте, что вы уже использовали stdin и stdout).Класс Debug принадлежит пространству именSystem.Diagnostics, что наводит на мысль о его применении: онупрощает диагностику, выявление и исправление ошибок. Debug.WriteLine направляет свой вывод слушателям трассировки — специальным классам, которые от- слеживают диагностический вывод ваших программ и направляют его наконсоль, вфайлы журналовилидиагностическиепрограммы, с об ираю щие данн ые для анали за. В:Могу ли я использовать конструкторы в своем коде? О:Разумеется. Конструктор — метод, который вызываетсяCLR при создании нового экземпляра объекта. Это самый обычный метод, в нем нет ничего мистического или специального. Вы можете добавить конструктор в любой класс, объявив метод без возвращаемого типа (без void, int или другого типа в начале), имя которого совпадает с именем класса. Каждый раз, когда средаCLRвстречает такой метод в классе,она распознает его как конструктор и вызывает каждыйраз при создании нового объекта и размещении его в куче. Позднее в этой главе кон­ структоры будут рассмо­ трены гораздо подробнее. Апокапростосчитайте,чтоконструктор — этоспециальныйметод,используемыйдля инициализации объектов. часто Задава ем ые вопросы РЕ Л А К С
278 глава 5 к ак хран ить сек реты Возможность некорректного использования объектов ПриложениеОуэна столкнулось с проблемами, потому что мы поспешно решили, что метод CalculateDamage должен вычислять величину повреждений.Оказалось, что вызывать этот метод напрямуюнебезопасно, потомучто он заменилзначениеDamage и стеррезультатыуже выполнен- ныхвычислений.Вместоэтогонужнобыло поручитьметодуSetFlamingвызватьCalculateDamage за нас, но даже этого было недостаточно, потому что мы также должны были позаботиться о том,чтобы метод SetMagic всегда вызывался первым. Итак, хотякласс SwordDamageработает с технической точки зрения,принепредвиденных обращениях к нему возникают проблемы. swordDamage.Roll = 12; swordDamage.SetMagic(false); swordDamage.SetFlaming(true); О б ъ е к т A p p ’ s Как должен был использоваться класс SwordDamage Класс SwordDamage предоставил приложению удобный метод для вычисления общих повреж- дений от меча. Все,что дляэтого нужно, — смоделироватьбросоккубиков,затем вызвать метод SetMagic и, наконец, вызвать метод SetFlaming.Если все делается именно в таком порядке, то полеDamageбудетобновлено сучетомвычисленных повреждений.Тем неменеевприложении всепроисходило нетак. Метод SetFlaming добавил поврежде- ния для огненного меча к полю Damage, но они были потеряны, когда метод CalculateDamage заменил содержимое п оля Dama ge. swordDamage.Damage поле содержит 17 Как использовался класс SwordDamage Вместо этого приложение задало значение поля Roll, затем вызвало SetFlaming, что привело кувеличению поврежденийв полеDamage. Далеебылвызван методSetMagic,инаконец,вызов метода CalculateDamage сбросил содержимое поля Damage с потерей дополнительных повреж- дений от ог ня. swordDamage.Damage поле содержит 15 О б ъ е к т S w o r d D a m a g e О б ъ е к т A p p ’ s О б ъ е к т S w o r d D a m a g e swordDamage.Roll = 12; swordDamage. SetFlaming(true); swordDamage.SetMagic(false); CalculateDamge();
дальше 4 279 инкапсуляция Инкапсуляция подразумевает ограничение доступа к части данных класса Проблемы некорректного использования объектов можно избе- жать: дляэтого нужнопозаботитьсяотом, чтобыкласс можнобыло использоватьтолько строго определеннымспособом. C#помогает вам вэтом, позволяяобъявить частьполейприватными(ключевое словоprivate).Донастоящего моментавывидели только открытые поля (public).Еслиполеобъекта объявлено открытым,то любой другой объект может прочитать или изменить это поле. Если же объявить полеприватным, токэтомуполю можнобудет обратиться только изэтогообъекта(илииздругогоэкземпляратого жекласса). class S wordDa mage { public const int B ASE_DA MAGE = 3; public const int F LAME_D AMAGE = 2; public int Roll; priv ate decimal magicMul tiplier = 1M; priv ate int flamingDamag e = 0; public int Damage; privat e void CalculateDamag e() { ... Объявляя метод CalculateDamage приватным, мы запрещаем приложению случайно вызы- вать его со сбросом поля Damage. Изменение полей, задействованных в вычислении, и пе- ревод их на объявление private не позволяет приложению вмешиваться в ход вычислений. Когда вы объявляете некоторые данные при- ватными, а затем пишете код для использо- вания этих данных, это называется инкапсу- ляцией. Когда класс защищает свои данные и предоставляет компоненты, простые в ис- пользовании и снижающие риск злоупотребле- ний, мы называем такой класс хорошо инкап- су лиро ва нны м. ин-кап-су-ли-ро-ван -ный, прил. снабженный защитным по- крытием или мембраной. Если вы хотитесделать поле приватным, достаточно вос- пользоваться ключевым сло- вом private при его объявлении. Тем самым вы сообщаетеC#, что у экземпляра SwordDamage операции записи и чтения полей magicMultiplier и flamingDamage могут выполняться только мето- дами экземпляраSwordDamage. Для других объектов они будут недоступны. Есливозникли сом­ нения — объявляй­ теприватным. Не до конца понимаете, как ре- шить, какие поляи методы стоит объявить приватными? Для на- чала объявите приватными все поляи методыи преобразуйтеих в открытые только тогда, когда это необходимо. В данном слу­ чае лень работает в вашу поль- зу. Если опустить объявление private или public, C#решит, что поле или метод являются при- ва тным и. А вы заметили, что мы так- же изменили имена приватных полей, чтобы они начинались со строчных букв? РЕ Л АКС
280 глава 5 шпион против шпиона Применение инкапсуляции для управления доступом к методам и полям класса Есливыобъявитевсеполя иметоды своего класса открытыми, любойдругойкласс сможетобратиться кним. Все нюансы того,что знаетиделаетвашкласс,становятсяоткрытойкнигойдлялюбого другого класса в программе... а вы только что видели, какэто может привестик совершенно непредвиденным отклонениям вповедении вашейпрограммы. Вотпочему ключевые словаpublicиprivateназываютсямодификаторамидоступа: ониизменяютуровень доступа к компонентам класса. Инкапсуляция позволяет вам управлять тем,что можетиспользоваться снаружи, а что должнооставаться скрытымврамкахвашего класса.Давайте посмотрим, какэтоработает. SecretAgent Alias RealName Password AgentGreeting СупершпионХербДжонс — объект, представляющий секретного агента в шпионской игре, действие которой происходит в 1960­е годы. Он за- щищаетсвободу, жизнь истремлениек счастью вкачествеагента под при- крытием вСССР.Объектявляется экземпляром классаSecretAgent. 1 АгентДжонс разработал план, который поможет ему избежать столкно- вения с объектом вражеского агента. Он добавил метод AgentGreeting, которыйполучаетвпараметрепароль. Еслипароль заданневерно,онрас- крываеттолько свойпсевдоним — DashMartin. 2 Вроде быабсолютно надежныйспособ защитыличностиагента, не такли? Еслиобъект агента,обращающийсясвызовом, не предоставит правильный пароль,имяагента остаетсянадежно защищенным. 3 AgentGreeting("the jeep is parked outside") Этот экземпляр EnemyAgent пытается обнаружить сверх- секретную личность нашего героического аг е нта. "Dash Martin" Враг узнает только псевдоним агента. Идеально!.. Правда? Вражеский агент указал неправильный пароль в приветствии. EnemyAgent Bo rs cht Vod ka ContactComrades OverthrowCapitalists RealName: "Herb Jones" Alias: "Dash Martin" Password: "the crow flies at midnight" S e c r e t A g e n t  E n e m y A g e n t S e c r e t A g e n t 
дальше 4 281 инкапсуляция public string RealName; public string Password; string iSpy = herbJones.RealName; Чтоможетсделать агент Джонс?Он может объявить поля приватными, чтобы держать свою личность всекрете от вражеских агентов.Еслиполе realName будет объявлено приватным, обратиться к нему можно будет только одним способом: вызвать методы, которыеимеют доступ к при­ ватным частям класса. private string realName; private string password; Ага! Он оставил поле открытым? Тогда зачем все эти хлопоты с подбором пароля для метода AgentGreeting? Я могу просто прочитать имя напрямую. Но ДЕЙСТВИТЕЛЬНО ЛИ поле RealName надежно защищено? Есливраг не знает пароль объекта SecretAgent,то настоящиеимена агентов вроде бывбезопасности.Но если данныехранятсяв открытых полях,никакойпользы оттакой«защиты» не будет: E n e m y A g e n t S e c r e t A g e n t  Объект EnemyAgent не может обратить- ся к приватным по- лям, потому что он является экземпля- ром другого класса. Объявлениеприватных методов и полей в калькуляторе повреждений предотвращает ошибки, связан - ные с попытками прямого обращения к ним. Но проблема все еще осталась! Если метод SetMagic будет вызван до метода SetFlaming, вы получите неправильныйрезультат. Может ли ключевое слово private предотвратить эту неприятность? Мозговой штурм Объявление полей открытыми означает, что к ним может обратиться (и даже изменить их!) любой другой объект. Достаточно заменить public на private, и поле становится скрытым от любого объекта, который не является экзем- пляром того же класса. Объявление полей и методов при- ватными гарантирует, что внешний код не сможет изменить используемыеданные в самый неподходящий момент. Мы переименовали поля, чтобы они начинались со строчных букв; это упрощает чтение кода.
282 глава 5 как храни ть секр еты В: Зачем создавать в объекте поля, которые другой объект не может прочитать или записать? О: Иногда в классе приходится хранить информацию, которая необходима для его работы, но должна оста- ваться скрытой от других объектов, — в ы уже видели пример такогорода. В предыдущей главебыло показано, что класс Random используетспециальные затравкидля инициализации генераторапсевдослучайныхчисел. Ока- зывается, во внутреннейреализации каждый экземпляр класса Random содержитмассив из несколькихдесятков чисел. Их использование гарантирует, что вызов Next всегда возвращает случайное число. Однакоэтотмассив является приватным —при создании экземпляраRandom вы не сможете обратиться к этому массиву. Если быэто быловозможно, то вымогли быразместить в своем объ- екте идентичный массив, и объект сталбы генерировать неслучайные значения. Следовательно, затравкидолжны быть полностью инкапсулированы. В: Хорошо, к приватным данным нужно обра- щаться через открытые методы. Что произойдет, если класс с приватным полем не дает мне воз- можности обратиться к этим данным, но моему объекту они очень нужны? О: Тогда вы не сможете к ним обратиться. Когда вы пишете класс, всегда позаботьтесь о том, чтобыудру- гих объектовбыла возможностьдобраться до нужных им данных. Приватные поля — очень важная часть инкапсуляции, но это лишь часть истории. Написание хорошо инкапсулированного класса означает, что вы предоставляетедругимлюдямсредствадля разумных ипростыхобращенийкнужнымданным,но ограждаете от нихкритическиеданные, откоторыхзависитваш класс. В: Я только что заметил, что командаGenerate method в IDE использует ключевое слово private. По чем у? О: Потому, что это самое безопасное, что может сделать IDE.Приватнымиобъявляются не только ме- т оды , с оздан ные к омандой Gene rate m ethod; ес ли вы сделаетедвойнойщелчокнаэлементе, чтобыдобавить обработчик событий, IDE и для него создаст приват- ный метод.Дело в том, что объявление приватного поля или метода предотвращает ошибки вроде тех, которыебыли продемонстрированы в калькуляторе повреждений.Вы всегда можете переобъявить поля и методы класса открытыми позднее, если данные должныбыть доступны для другогокласса. К приватным полям и методам могут обращаться только экземпляры того же класса У объекта есть только одна возможность обратитьсякданным, хранящимся в приватных полях другого класса:он должен воспользоваться открытыми полями и методами, которые возвращают данные. Агентам EnemyAgent иAlliedAgentпридетсяиспользовать методAgentGreeting, нодружественные агенты, которые также являютсяэкземплярамиSecretAgent, все увидят...пото- му что любойкласс можетпросматривать приватныеполя всехостальных экземпляров того же класса. AgentGreeting("the crow flies at midnight") "Herb Jones" Класс AlliedAgent пред- ставляет агента из со- юзной страны, которому разрешено знать истин- ную личность секретного агента. Но экземпляры AlliedAgent все равно не имеют доступа к при- ватным полям объекта SecretAgent. Они видны только другому объекту SecretAgent. Чтобы один объект мог по- лучить доступ к приватному полю другого объекта другого класса, он дол- жен воспользо- ваться откры- тыми методами, которые воз- вращают эти данные. A l l i e d A g e n t S e c r e t A g e n t  Другие экземпляры SecretAgent видят приватные компо- н енты кл асса. В сем остальным объектам приходится исполь- зовать открытые компоненты. часто Задава ем ые вопросы
дальше 4 283 инкапсуляция Чтобы немного потренироваться в использовании ключевого слова private, мы создадим ма- ленькую игру «Больше-меньше». Игра начинается со ставки 10 долларов и выбирает случайное число от 1 до 10. Игрок пытается угадать, будет следующее число больше или меньше текущего. Если игрок угадал правильно, он выигрывает доллар, в противном случае он теряет доллар. За- тем следующее число становится текущим, и игра продолжается. Создайте для игры новое консольное приложение. Метод Main выглядит так: public static void Main(string[] args) { Console.WriteLine("Welcome to HiLo."); Console.WriteLine($"Guess numbers between 1 and {HiLoGame.MAXIMUM}."); HiLoGame.Hint(); while (HiLoGame.GetPot() > 0) { Console.WriteLine("Press h for higher, l for lower, ? to buy a hint,"); Console.WriteLine($"or any other key to quit with {HiLoGame.GetPot()}."); char key = Console.ReadKey(true).KeyChar; if (key == 'h') HiLoGame.Guess(true); else if (key == 'l') HiLoGame.Guess(false); else if (key == '?') HiLoGame.Hint(); else return; } Console.WriteLine("The pot is empty. Bye!"); } Теперь добавьте статический класс с именем HiLoGame и включите в него следующие компоненты. Так как класс является статическим, все его компоненты тоже должны быть статическими. Не забудьте включить ключе- вое слово public или private в объявление каждого поля и метода: 1. Целочисленная константа MAXIMUM, по умолчанию равная 10. Не забывайте, что ключевое слово static не может использоваться с константами. 2. Экземпляр Random с именем random. 3. Целочисленное поле с именем currentNumber, инициализируемое первым случайным числом, которое дол- жен отгадать игрок. 4. Целочисленное поле с именем pot, в котором хранится размер ставки. Объявите это поле приватным. 5. Метод с именем Guess, получающий параметр bool с именем higher. Метод должен делать следующее (чтобы понять, как он вызывается, присмотритесь к методу Main): • Он выбирает следующее случайное число, которое должен угадать игрок. • Если игрок выбрал «больше», а следующее число >= текущемуИЛИ если игрок выбрал «меньше», а следую- щее число <= текущему, выведите на консоль сообщение «You guessed right!» и увеличьте ставку. • В противном случае выведите на консоль сообщение «Bad luck, you guessed wrong» и уменьшите ставку. • Замените текущее число выбранным в начале метода, а затем выведите на консоль сообщение «The current number is» и число. 6. Метод с именем Hint, который находит половину максимума, а затем выводит на консоль сообщение «The number is at least {half}» или «The number is at most {half}» и уменьшает ставку. Не забывайте: подсмотреть в решение — не значит жульничать! Мы объявляем pot приватным, потому что не хотим, чтобы другие классы могли добавлять деньги, однако методу Main все равно нужно иметь возможность вывести размер ставки на консоль. Вни- мательно присмотритесь к коду метода Main — сможете ли вы понять, как сделать значение pot доступным для метода Main, не давая ему возможности присвоить значение этого поля? Упражнение
284 глава 5 оставляя место воображению Ниже приведен остальной код игры «Больше-меньше». Игра начинается со ставки 10 долларов и выбирает случайное число от 1 до 10. Игрок пытается угадать, будет ли следующее число большеили меньшетекущего. Если игрок угадал правильно, он выигрывает доллар, в противном случае он теряет доллар. Затем следующее число становится текущим, и игра продолжается. Код класса HiLoGame: static class HiLoGame { public const int MAXIMUM = 10; private static Random random = new Random(); private static int currentNumber = random.Next(1, MAXIMUM + 1); private static int pot = 10; public static int GetPot() { return pot; } public static void Guess(bool higher) { int nextNumber = random.Next(1, MAXIMUM + 1); if ((higher && nextNumber >= currentNumber) || (!higher && nextNumber <= currentNumber)) { Console.WriteLine("You guessed right!"); pot++; } else { Console.WriteLine("Bad luck, you guessed wrong."); pot--; } currentNumber = nextNumber; Console.WriteLine($"The current number is {currentNumber}"); } public static void Hint() { int half = MAXIMUM / 2; if (currentNumber >= half) Console.WriteLine($"The number is at least {half}"); else Console.WriteLine($"The number is at most {half}"); pot--; } } Дополнительный вопрос: открытое поле random можно заменить новым экземпляром Random, инициализиро- ванным другой затравкой.Тогда новый экземпляр Random можно будет использовать с той же затравкой, чтобы узнать числа заранее! HiLoGame.random = new Random(1); Random seededRandom = new Random(1); Console.Write("The first 20 numbers will be: "); for(inti=0;i<10;i++) Console.Write($"{seededRandom.Next(1, HiLoGame.MAXIMUM + 1)}, "); Метод Hint должен быть открытым, потому что он вызы- вается из Main. А вы заметили, что в командуif/else не были включены фигурные скобки? В секции if или else, состоящей из одной строки, фигурные скобки не нужны. При попытке добавить ключевое слово static к константе вы получите ошибку компилятора, потому что все константы являются статическими. Попробуйте добавить его в своем классе — вы сможете обратиться к нему из другого класса, как к любому другому статическому полю. Это хороший пример инкапсуля- ции. Вы защитили поле pot, объ- явив его приватным. Оно может изменяться вызовами методов Guess и Hint, а метод GetPot предоставляет доступ только для чтения. Всеэкземпляры Random, инициализи- рованные одной затравкой, генериру- ют одну последовательность псевдо- случай ны х чисел . Поле pot объявлено приватным, но метод Main может использовать метод GetPot для получения его значения без возможности изменять его. Важный момент. Вы- делите несколько минут и хорошенько разбери- тесь в том, как работа- ет этот механизм. Упражнение Решение
дальше 4 285 инкапсуляция Потому что иногдабывает нужно, чтобы класс скрывал информацию от остального кода программы. Многимразработчикам идея инкапсуляциикажется стран- ной, потомучто идеясокрытияполей, свойствилиметодов одного класса от другогоклассавыглядит немного противо- естественно. Есть очень веские причины, по которым вы должны тщательно продумать, какая информация должна раскрываться для остальнойпрограммы. Что-то здесь не так. Если я объявлю поле приватным, используя его в другом классе, то моя программа просто не будет компилироваться. Но если заменить «private» на «public», моя программа снова построится! Получается, что добавление «private» может только нарушить работу моей программы. Инкапсуляция означает, что один класс скрывает информацию от другого. Сокрытие информации способствует предотвращению ошибок в программах. Тогда для чего мне объявлять поле приватным?
286 глава 5 инкапсуляция и ложнаябезопасность Будьте осторожны! Инкапсуляция — не то же самое, что безопасность. Приватные поля не защищены. Есливыстроите игру про шпионов, инкапсуляция поможет избежать ошибок. Но если вы стро- ите программу для настоящих шпионов, инкапсуляция не обеспечивает защиты данных. Например, вернемся к игре «Больше-меньше» . Установите точку прерывания в первой строке метода Main, добавь- те отслеживание для HiLoGame.random и проведите отладку программы. Развернув раздел Non-Public Members, вы увидите внутренние аспекты класса Random, включая массив _seedArray, используемый для генерирования псевдо- случайных чисел. Не только IDE видит приватные компоненты вашего класса. В .NET существует механизм отражения (reflection), который позволяет писать код для обращения к объектам в памяти и просмотра их содержимого — даже приват- ных полей. Рассмотрим краткий пример его использования. Создайте новое консольное приложение и добавьте класс с именем HasASecret: class HasASecret { // Класс содержит поле secret. Обеспечит ли ключевое слово private его защиту? private string secret = "xyzzy"; } Классы отражения принадлежат пространству имен System.Reflection, поэтому в файл с методом Main следует добавить команду using: using System.Reflection; Ниже приведен главный класс с методом Main, который создает новый экземпляр HasASecret, а затем использует отражение для чтения поля secret. Он вызывает GetType — метод, который можно вызвать для любого объекта, чтобы получить информацию о его типе: class MainClass { public static void Main(string[] args) { HasASecret keeper = new HasASecret(); // При снятии комментария с команды Console.WriteLine происходит ошибка компиляции: // поле 'HasASecret.secret' недоступно из-за его уровня защиты. // Console.WriteLine(keeper.secret); // Но для получения значения поля secret все еще можно воспользоваться отражением FieldInfo[] fields = keeper.GetType().GetFields( BindingFlags.NonPublic | BindingFlags.Instance); // Этот цикл foreach выводит на консоль строку "xyzzy" foreach (FieldInfo field in fields) { Console.WriteLine(field.GetValue(keeper)); } } } У каждого объекта имеется метод GetType, который возвращает объект Type. Метод Type.GetFields возвращает массив объектов FieldInfo, по одному длякаждого поля. Каждый объект FieldInfo содержит информацию о своем поле. Если вызвать метод GetValue с экземпляром этого объекта, он вернет значение, хранящееся в поле, — даже если это поле объявлено приватным.
дальше 4 287 инкапсуляция Для чего нужна инкапсуляция? Представьте объект в виде «черного ящика»... Иногда программисты говорят обобъектахкак о«черныхящиках», и это вполне неплохая точка зрения. Когда мыговорим, что нечто является «черным ящиком», мы имеем в виду, что мы видим его поведение, но не можем получить информацию о том, какон устроен. Привызовеметода объекта вас не интересует, как работает этот метод, — по крайней мерепока.Важнолишь то, что методполучает входные данные, которые вы емупередаете, и делаетто, что емуположено делать. O B J E C T Когдаразработчики говорят о «черном ящике», они имеют в виду нечто, скрывающее свои внутренние механизмы; чтобы пользоваться ими, не нужно знать, как они работают. Если «черный ящик» выполняет всего одну операцию и ему не нужно передавать параметры, он становится программным аналогом черной коробочки с единственной управляющей кнопкой. К ящику можно добавить другиеэлементыуправления, например окно, в котором можно увидеть,что происходит внутри,или рукояткиирычаги для управления внутреннейреализацией. Но если они не делаютничего, что былобы необходимо для вашейсистемы,то никакойпользыот них небудети они только создаютлишниепроблемы. С инкапсуляцией ваши классы... Ì Easier to use. Выуже знаете, что поля используются классами для отслеживания их состояния. Многиеклассыиспользуют методы для поддержания полей в актуальном состоянии — методы, которыеникогда не будутвызываться другимиклассами. Достаточно часто встречаются классы с полями,методамиисвойствами,которыеникогда небудутвызыватьсядругимиклассами. Если вы объявитеэти компоненты приватными,то они непоявятся вокне IntelliSenseпозднее, когда вы будете работать с этим классом. IDE небудет загромождаться лишнимикомпонентами, а это упростит использованиекласса. Ì Less pronetobugs. Ошибка в программеОуэна произошла из-за того, что приложение вызыва- ло метод напрямую, вместо того чтобыдоверить его вызов другимметодам класса. Еслибы этот методбыл объявленприватным,то этойошибкиможнобылобы избежать. Ì Flexible. Нередковамприходитсявозвращаться кпрограмме, написаннойужедавно, идобавлять внееновуюфункциональность. Есливашиклассы хорошоинкапсулированы,то вы будететочно знать,как пользоваться имиирасширять позднее. Как построение плохо инк апсулированного класса может усложнить изменение вашей программы в будущем? Мозговой штурм
288 глава 5 вопросы, которые помогают определиться с инкапсуляцией Ì Все поля и методы вашего класса объявлены открытыми? Если ваш класс не содержит ничего, кроме открытых полей и методов, вероятно, вам стоит дополнительно подумать об инкапсуляции. Ì Подумайте о возможностях некорректного использования полей и ме- тодов кл ас са. С какими проблемами вы можете столкнуться, если поля не инициализированы или методы вызываются некорректно? Ì Какие поля требуют дополнительной обработки или вычислений при присваивании им значений? Это главные кандидаты для инкапсуляции. Если кто-то позднее напишет метод,который изменит значение водном из такихполей, это может создать проблемы для той работы, которую пытается выполнить ваша программа. Мы использовали константы для базовых и огненных повреждений. Объявление их открытыми не создаст проблем, потому что константы не могут изменяться. Но так как они не используются другим классом, возможно, их можно объявить приватными. Ì Объявляйте поля и методы открытыми только при необходимости. Если у вас нет веских причин объявлять что-то открытым, не делайтеэтого — объявле- ние всех полей программы открытыми может сильно усложнить вашу жизнь. Впрочем, не старайтесь объявлять все поля и методы приватными. Если вы заранее подумаете над тем, какие поля должны быть открытыми, а какие — нет, вы сэкономите себе много времени в будущем. Несколько идей для инкапсуляции классов
дальше 4 289 инкапсуляция Если вы хорошо инкапсулируете свои классы сегодня, это сильно упростит их повторное использование завтра. И как и в шахматах, существует почти неограниченное число возможных страте- гий инкапсуляции! Но хорошо инкапсулированный класс делает то же самое, что и плохо инкапсулированный! Точно! Различия в том, что хорошо инкапсулированный класс строится так, чтобы предотвратить ошибки и упростить его испол ьзо вание . Хорошо инкапсулированный класс можно легко превратить в плохо инкапсулированный: проведите поиск и замените каждое вхождение private на public. Здесь проявляетсяоднаизособенностей ключевогослова private:обычно выможете взять любую программу, провестипоиск сзаменой,ипрограм- ма будет компилироваться и работать точно так же, как прежде! И это однаизпричин, почемунекоторымпрограммистамбывает трудно понять суть инкапсуляции, когда онисталкиваютсяс нейвпервые. Когдавы возвращаетеськ коду,к которомудавно неприкасались, легко забыть, как ондолжениспользоваться. Инкапсуляцияспособнаоченьсильноупростить вам жизнь! Донастоящего моментавкнигеречь шла отом, какзаставитьпрограммы что­тоделать, т.е . проявлять некоторое поведение. Инкапсуляцияработа- ет немного иначе. Она не влияетна поведение вашейпрограммы, скорее онаотноситсяк«шахматной» стороне программирования: выскрываете некоторую информацию всвоих классахвпроцессеих проектирования ипостроенияи темсамымформируетестратегию взаимодействиясними. Чем лучше стратегия, тем более гибкой и простой в сопровождении будетваша программа и тембольшего количества ошибоквы избежите.
290 глава 5 ¢ Всегда думайте о причинах возникновения ошибки, прежде чем пытаться исправлять ее. Не жалейте вре- мени и постарайтесь действительно хорошо понять, что же происходит в программе. ¢ Добавление команд вывода может быть эффектив- ным средством отладки. Используйте метод Debug. WriteLine для добавления команд вывода диагности- ческой информации. ¢ Конструктор — метод, вызываемый CLR при созда- нии нового экземпляра объекта. ¢ Строковая интерполяция упрощает чтение операций конкатенации. Чтобы использовать ее, поставьте $ в начало строки и заключите выводимые значения вфи- гурные скобки { }. ¢ КлассSystem.Console направляет свой вывод в стан- дартные потоки, обеспечивающие ввод и вывод в консольных приложениях. ¢ Класс System.Diagnostics.Debug передает свои резуль- таты слушателям трассировки (специальным клас- сам, выполняющим конкретные действия с диагности- ческим выводом).Одинизнихнаправляет вывод в окно IDE Output (Windows) или Application Output (macOS). ¢ Люди не всегда используют ваши классы точно так, как вы ожидаете. Механизм инкапсуляции обеспечивает гибкость компонентов класса и затрудняет их некор- ректное использование. ¢ Инкапсуляция обычно подразумевает использование ключевого слова private для закрытого хранения не- которых полей и методов класса, чтобы они не могли некорректно использоватьсядругими классами. ¢ Когда класс защищает свои данные и предоставляет поля и методы, безопасные в использовании и затруд- няющие их некорректное использование, такой класс называется хорошо инкапсулированным. SwordDamage Roll MagicMultiplier FlamingDamage Damage CalculateDamage SetMagic SetFlaming Хорошо, мы знаем, что в коде приложения для расчета повреждений есть проблемы. И что с этим делать? Помните, как мы использова- ли метод Debug.WriteLine ранее в этой главе, чтобы найти ошиб- ку в приложении? Выяснилось, что класс SwordDamage нормально работает только тогда, когда его методы вызываются в стро- го определенном порядке. Вся эта глава посвящена инкапсуляции, поэтому можно ожидать, что инкапсуляция будет использована для решения этой проблемы. Но... как именно? КЛЮЧЕВЫЕ МОМЕНТЫ используйте инкапсуляцию для защиты классов
дальше 4 291 инкапсуляция Воспользуемся инкапсуляцией для улучшения класса SwordDamage Мырассмотрели некоторые идеи для инкапсуляции классов. По- смотрим, удастсялинамприменитьихв классеSwordDamage, чтобы предотвратитьвозможнуюпутаницу излоупотреблениясостороны приложения, вкотороебылвключенэтоткласс. Все компоненты класса SwordDamage объявлены открытыми? Да, это так . Четыре поля (Roll, MagicNumber, FlamingDamage иDamage)объявленыоткрытыми,какитриметода(CalculateDamage, SetMagic иSetFlaming). Стоит подумать об инкапсуляции. Поля или методы используются некорректно? Безусловно. В первой версии калькуляторабыл вызван метод CalculateDamage там,где его вызовследовало бы поручитьметоду SetFlaming. Даже наша попытка исправить ошибку завершилась неудачей, потому что методы использовались некорректно из-за того,что онивызывались внеправильном порядке. Требуются ли вычисления после присваивания значения поля? Несомненно. После присваивания поляRoll экземпляр должен рассчитать повреждения немедленно. Какие же поля и методы должны быть открытыми? Отличный вопрос. Выделите несколько минут и поразмышляйте над ответом. Мы ещевернемсяк немув концеглавы. П оразмыс лите н ад э тими воп рос ами, а затем еще раз взгляните на класс SwordDamage. Что бы вы сделали, чтобы исправить проблемы в классе SwordDamage? Мозговой штурм Объявление ком- по нен тов кла сса приватными мо- жет предотвратить ошибки, связанные с вызовом откры- тых методов из других классов или обновлением его открытых полей непредвиденными способами.
292 глава 5 Инкапсуляция обеспечивает безопасность данных Выужеузнали, как ключевое слово private защищает компоненты класса от прямых обращений и как предотвратить ошибки, вызванные непредвиденными вызовами методов или обновлениями по- лей, — подобно тому, какметодGetPotв игре«Больше-меньше» предоставлял доступ только для чтения кприватному полюpot иэтополемогло изменятьсятолько методамиGuessилиHint. Следующий класс работаетпо тому жепринципу. Инкапсуляция в классе Построим классPaintballGun для видеоигры — симулятора пейнтбольной арены. Игрок может подо- брать магазинсшариками иперезарядить его в любой момент, поэтому мы хотим, чтобы класс отсле- живал общее количество шариков у игрока и текущее число заряженных шариков. Мы добавим метод, которыйпроверяет,непуст лимаркер иненужно лиегоперезарядить.Такженеобходимо отслеживать размер магазина. Каждыйраз, когда игрокполучаетновыебоеприпасы, маркер долженавтоматически перезаряжать полныймагазин. Чтобыобеспечить гарантированную перезарядку,мыопределимметод для назначения количества шариков, из которого будет вызываться метод Reload. class PaintballGun { public const int MAGAZINE_SIZE = 16; private int balls = 0; private int ballsLoaded = 0; public int GetBallsLoaded() { return ballsLoaded; } public bool IsEmpty() { return ballsLoaded == 0; } public int GetBalls() { return balls; } public void SetBalls(int numberOfBalls) { if (numberOfBalls > 0) balls = numberOfBalls; Reload(); } public void Reload() { if (balls > MAGAZINE_SIZE) ballsLoaded = MAGAZINE _SIZE; else ballsLoaded = balls; } public bool Shoot() { if (ballsLoaded == 0) return false; ballsLoaded--; balls--; return true; } } Игра должна иметь возможность задать количество ша- риков. Метод SetBalls защищает поле balls, разрешая игре задать только положительное число. Затем он вызывает Reload, чтобы автоматически перезарядить маркер. Перезарядить маркер можно только одним способом: вызвать метод Reload, который за- ряжает полный магазин или заряжает остав- шиеся шарики, если их не набирается на пол- ный магазин. Тем самым предотвращается рассинхронизация полей balls и ballsLoaded. Метод Shoot возвращает true и уменьша- ет поле balls, если маркер заряжен, или false в противном случае. Когда игре потребуется вы- вести количество оставшихся шариков и количество за- ряженныхшариков в поль- зо вател ь ском ин тер фей се, она может вызвать методы GetBalls и GetBallsLoaded. Упрощает ли метод IsEmpty чтение этого кода? Или он избыточен? Правиль- ного или неправильного ответа не существует — можно привести аргументы в пользу как одной, так и другой позиции. Константа будет объявлена от- крытой, потому что она будет использоваться методом Main. класс инкапсулирован, но не идеально
дальше 4 293 инкапсуляция Консольное приложение для тестирования класса PaintballGun Опробуем новыйклассPaintballGun. Создайтеновоеконсольное приложениеи добавьте внегокласс PaintballGun.Ниже приведенметодMain — внемиспользуетсяциклдлявызоваразличныхметодов класса: static void Main(string[] args) { PaintballGun gun = new PaintballGun(); while (true) { Console.WriteLine($"{gun.GetBalls()} balls, {gun.GetBallsLoaded()} loaded"); if (gun.IsEmpty()) Console.WriteLine("WARNING: You're out of ammo"); Console.WriteLine("Space to shoot, r to reload, + to add ammo, q to quit"); char key = Console.ReadKey(true).KeyChar; if (key == ' ') Console.WriteLine($"Shooting returned {gun.Shoot()}"); else if (key == 'r') gun.Reload(); else if (key == '+') gun.SetBalls(gun.GetBalls() + PaintballGun.MAGAZINE _SIZE); else if (key == 'q') return; } } Наш класс хорошо инкапсулирован, но... Классработает, ион достаточнохорошо инкапсулирован.Полеballsзащищено: оно неможет содержать отрицательное количество шарикови синхронизируетсясполемballsLoaded.Методы Reload иShoot ра- ботаюттак, как ожидалось, инет никакихочевидныхвозможностейнекорректно использовать этот класс. Но давайтеприсмотримсяк следующей строке метода Main: else if (key == '+') gun.SetBalls(gun.GetBalls() + PaintballGun.MAGAZINE _SIZE); Будем откровенны: такой синтаксис менееудобен,чем синтаксис работы с полями. Еслибы мыработа- ли с полем,то можно былобы воспользоваться оператором += для увеличения его на размер магазина. Инкапсуляцияполезна,но мыне хотим, чтобыс классомбыло неудобно илитрудно работать. Можно ли как-то обеспечить защиту поля balls, но при этом пользоваться удобным синтак- сисом +=? Консольное приложение с циклом, в котором те- стируется экземпляр класса, должно быть вам уже хорошо знакомо. Убедитесь в том, что вы можете прочитать код и понимаете, как он работает. Регистр символов в именах приватных и открытых полей Мы использовали схему «верблюжий регистр» (camelCase) для приватных полей и схему «регистр Pascal» (PascalCase) для открытых полей. Схема PascalCase озна- чает, что каждое слово в имени переменной начинается с буквы верхнего регистра. Схема camelCase похожа на PascalCase, но в ней имя начинается с буквы нижнего регистра. Она называется «верблюжьим регистром», потому что буквы верхнего регистра напоминают горбы верблюда. Использование разных схем для открытых и приватных полей — соглашение, ко- торое соблюдают многие программисты. Последовательное применение регистра в именах полей, свойств, переменных и методов упростит чтение вашего кода. Сделайте э то!
294 глава 5 Донастоящего момента вы узнали о двухразновидностях компонентов класса: методах и полях. Так- же существует третьяразновидность компонентов класса, используемая для инкапсуляции: свойства. Свойство — компонент класса, который выглядит как поле прииспользовании, но ведет себя как метод. Свойство объявляется как поле, с типом и именем, но вместо символа ; за объявлением следует пара фигурныхскобок. В скобках определяются методы доступа — методы для чтения или присваивания значения свойства. Существуют дверазновидностиметодовдоступа: Ì get­метод возвращаетзначение свойства. Он начинается с ключевого слова get,за которым сле- дуетметод в фигурныхскобках. Метод должен возвращать значение,соответствующее типу из объявления свойства. Ì set­методзадаетновоезначениесвойства. Онначинаетсяс ключевого слова set,за которым сле- дует метод в фигурныхскобках. Внутриметода ключевое слово value представляет переменную, доступную только для чтения,котораясодержит присваиваемоезначение. Свойство очень часто читает или задает резервное поле — так мы называем приватноеполе, инкапсу- лируемое ограничением доступа к нему через свойство. Замена методов GetBalls и SetBalls свойством Нижеприведены методы GetBalls и SetBalls из класса PaintballGun: public int GetBalls() { return balls; } public void SetBalls(int numberOfBalls) { if (numberOfBalls > 0) balls = numberOfBalls; Reload(); } Заменим ихсвойством. Удалите оба метода и добавьте свойство Balls. public int Balls { get { return balls; } set { if (value > 0) balls = value; Reload(); } } Свойства упрощают инкапсуляцию Замените! Это объявление. Из него следует, что свойство объявляется с именем Balls и типом int. Get-метод идентичен методуGetBalls, который он заменяет. Set-метод почти идентичен методу SetBalls. Существует только одно различие: в нем ис- пользуется ключевое слово value там, где в SetBalls использовался параметр. Ключевое слово value всегда содержит значение, при- сваиваемое set-методом. Старый метод SetBalls получал параметр int с именем numberOfBalls, в кото- ром передавалось новое значение резервного поля. Set-метод использует клю- чевое слово «value» везде, где метод SetBalls использовал numberOfBalls. сво йства к ласса
дальше 4 295 инкапсуляция Изменение метода Main для использования свойства Balls Теперь, когда вызаменилиметоды GetBalls иSetBalls однимсвойствомс именемBalls, вашкодстроиться небудет. Необходимо обновить метод Main,чтобы внем использовалось свойствоBalls вместо старых мето до в. Метод GetBalls вызывался в команде Console.WriteLine: Console.WriteLine($"{gun.GetBalls()} balls, {gun.GetBallsLoaded()} loaded"); Проблемарешается заменой GetBalls() на Balls — когда это будетсделано,команда будетработать так же,как прежде. Теперь найдите другую точку,вкоторойиспользовались методы GetBalls иSetBalls: else if (key == '+') gun.SetBalls(gun.GetBalls() + PaintballGun.MAGAZINE _SIZE); Этотасамаястрока кода, такая некрасиваяи громоздкая. Свойства оченьполезны, потомучтоонирабо- таюткак методы,но используются как поля. Теперь используем свойствоBalls какполе— замените эту строку командой, в которой оператор += используется так же,как если бы свойство Ballsбыло полем: else if (key == '+ ') gun.Balls += PaintballGun.MAGAZINE _SIZE; Обновленный метод Main выглядит так: static void Main(string[] args) { PaintballGun gun = new PaintballGun(); while (true) { Console.WriteLine($"{gun.Balls} balls, {gun.GetBallsLoaded()} loaded"); if (gun.IsEmpty()) Console.WriteLine("WARNING: You're out of ammo"); Console.WriteLine("Space to shoot, r to reload, + to add ammo, q to quit"); char key = Console.ReadKey(true).KeyChar; if (key == ' ') Console.WriteLine($"Shooting returned {gun.Shoot()}"); else if (key == 'r') gun.Reload(); else if (key == '+ ') gun.Balls += PaintballGun.MAGAZINE _SIZE; else if (key == 'q') return; } } Отладка класса PaintballGun поможет понять, как работает свойство Отладчик поможетлучшепонять,как работаетновоесвойствоBall: Ì Установитеточку прерывания вфигурныхскобкахget-метода (return balls;). Ì Установитедругую точку прерывания впервой строкеset-метода (if(value > 0)). Ì Установите точку прерывания в начале метода Main и начните отладку. Начните выполнение впошаговом режиме. Ì Привыполнениикоманды Console.WriteLine сработает точка прерывания вget-методе. Ì Продолжайте выполнение в пошаговом режиме с обходом методов. Привыполнении метода += сработает точка прерывания в set-методе. Добавьте отслеживание для резервного поля balls иключевого слова value. Обновите! Если бы свойство Balls было полем, то вы бы именно так использовали оператор += для его обновления. Свойства используются точно так же.
296 глава 5 Автоматически реализуемые свойства упрощают ваш код Чрезвычайнораспространенныйсценарийиспользованиясвойств — созданиерезервного поляи опре- делениеget-иset-методовдоступа. Создадим новоесвойствоBallsLoaded,котороеиспользует существу­ ющее полеballsLoaded вкачестверезервного. private int ballsLoaded = 0; public int BallsLoaded { get { return ballsLoaded; } set { ballsLoaded = value; } } Теперь удалите метод GetBallsLoaded и измените метод Main, чтобы в нем использовалось свойство: Console.WriteLine($"{gun.Balls} balls, {gun.BallsLoaded} loaded"); Сновазапуститепрограмму.Она должна работать точно также,как ипрежде. Использование фрагмента кода для создания автоматически реализуемого свойства Автоматическиреализуемое свойство, иногданазываемоеавтоматическим свойством, представляет собойсвойство cget-методом, которыйвозвращаетзначение резервного поля, иset-методом, который егообновляет.Иначе говоря, оноработает точнотакже,как только чтосозданное свойствоBallsLoaded. Впрочем, существует одно важноеразличие: при созданииавтоматического свойстварезервноеполе не определяется. Вместоэтого компиляторC#создаетрезервноеполеза вас,и любыеоперациис ним могутвыполнятьсятолько черезget-и set-методы. VisualStudioпредоставляет очень полезный инструмент длясозданияавтоматическихсвойств:фрагмент кода — маленький блоккода, которыйIDE автоматическивставляетв вашу программу.Воспользуемся фрагментом кода для создания автоматического свойства BallsLoaded. Удалите свойство BallsLoaded и резервное поле. Удалите добавленное вами свойство BallsLoaded, потому что мы заменим его автоматическиреализуемым свойством. Затем удалите резервноеполеballsLoaded(private int ballsLoaded = 0;), потому что при созданииавтоматиче- ского свойства компилятор C#генерируетскрытоерезервноеполе за вас. Прикажите IDE вставить фрагмент кода для свойства. Установите курсор в позицию, где на- ходилось поле, введите prop и дважды нажмите клавишу Tab, чтобы вставить фрагмент. IDE добавитвваш кодследующую строку: Фрагменткода представляет собой шаблон,части которого можно редактировать, — фрагмент позволяет изменить тип и имя свойства. Нажмите клавишуTab, чтобы переключиться на имя свойства,изменитеимя на BallsLoadedи нажмитеEnter, чтобы завершитьфрагменткода: public int BallsLoaded { get; set; } Внесите исправления в остальном коде класса. Так как вы удалили поле ballsLoaded, класс PaintballGunснова некомпилируется. Проблема легкорешается— поле ballsLoaded встречается в кодепять раз(одинраз вметодеIsEmpty ипо два раза в методах ReloadиShoot).Замените эти имена на BallsLoaded— программа сноваработает. 1 2 3 Добавьте! Это свойство использует приватное резервное поле. Его get-метод возвра- щ ает значен ие, со де ржащ ееся в пол е, а set-метод обновляет его. Объявлять резервное поледля автоматического свойства не нужно, потому что компилятор C# создаст его автоматически. фрагменты кода
дальше 4 297 инкапсуляция Использование приватного set-метода для создания свойств, доступных только для чтения Взглянитеещеразна только что созданноеавтоматическоесвойство: public int BallsLoaded { get; set; } Онобезусловно становится хорошей заменойдлясвойства сget-и set-методами,которыепростообнов- ляютрезервноеполе. Автоматическоесвойствонамного лучше читается исодержитменьшекода, чем поле ballsLoadedи методGetBallsLoaded.Всестало лучше,верно? Возникает только одна проблема: мы нарушили инкапсуляцию. Вся схема приватного поля с открытым методом создавалась для того, чтобы количествозаряженныхшариковбыло доступно только длячтения. Метод Main может легко задать свойство BallsLoaded. Мы объявилиполе приватным и создали откры- тыйметоддля чтения значения,чтобы его можнобыло изменить только внутриклассаPaintballGun. Объявление set-метода BallsLoader приватным Ксчастью,класс PaintballGunможноснова сделать хорошоинкапсулированным.Дляэтого достаточно поставить модификатор доступа перед ключевым словом get илиset. Чтобы свойство стало доступным только для чтения (т. е. его значениенельзя будет задать из другого класса),используйтевего set-методемодификатор private.Собственно,для обычныхсвойствset-метод можно вообщеопустить, но это не относитсяк автоматическим свойствам, которыедолжны иметь set- метод,без которого код небудеткомпилироваться. Объявим set-метод приватным: public int Ba llsLoa ded { get; pri vate set; } Теперь полеBallsLoadedдоступно только для чтения. Его можно прочитать откуда угодно, но обнов- ляться оно можеттолько из класса PaintballGun. КлассPaintballGunснова хорошо инкапсулирован. Чтобы сд елать авто- матическое свойство доступным только для чтения, объявите set- метод приватным. В: Мы заменили методы свойства- ми. Существуют ли какие-то различия между тем, как работают методы и get/ set-методы свойств? О: Нет. Get- и set-методы доступа просто являются особой разновидностью мето- дов — для других объектов они ничем не отличаются от полей и вызываются при каждом «присваивании» значения поля. Get-методы всегда возвращают значение, тип которого совпадает с типом поля. Set- метод работает точно так же, как работал бы метод с одним параметром value, тип которого соответствует типу поля. В: Значит, свойство может содержать ЛЮБЫЕ команды? О: Абсолютно. Все, что можно сделать в методе, можно сделать и в свойстве — в негодажеможновключить сложнуюлогику, котораяделает все,чтоможет делать обыч- ныйметод.Свойствоможетвызыватьдругие методы, обращаться кдругимполям, даже создавать экземпляры объектов. Помните, что get/set-методы вызываются только при обращении к свойству, так что они должны включать лишь те команды, которые от- носятся к чтению/записи свойств. В: Зачем включать сложную логику в get- или set-метод?Ведь это всего лишь еще один способ изменения полей? О: Потому что иногда при каждом при- сваивании поля приходится выполнять не- которые вычисления или операции. Вспом- нитепроблемуОуэна — она возниклаиз-за того, что приложение не вызвало методы Swo rdD amag e в правил ь ном поряд ке пос ле присваиваниязначенияRoll.Еслизаменить все методы свойствами, можно гарантиро- вать, что set-методы будут правильно вы- числять повреждения.(Собственно, именно это будетсделано в конце главы!) часто Зад авае м ые вопросы
298 глава 5 А если потребуется изменить размер магазина? Вданный моментклассPaintballGunиспользуетconst для определения размера магазина: public const int MAGAZINE_SIZE = 16; Аесливызахотите,чтобы игра задавала размер магазина при созданииэкземпляра маркера?Заменим константу свойством. Удалите константу MAGAZINE_SIZE и замените ее свойством, доступным только для чтения. public int MagazineSize { get; private set; } Измените метод Reload, чтобы в нем использовалось новое свойство. if (balls > MagazineSize) BallsLoaded = MagazineSize; Исправьте в методе Main строку, в которой добавляются боеприпасы. else if (key == '+ ') gun.Balls += gun.MagazineSize; Но тут возникает проблема... как инициализировать MagazineSize? КонстантеMAGAZINE_SIZE присваивалось значение16. Теперь константа заменена автоматическим свойством, ипри желании мы моглибы инициализировать еезначением16, каки поле, — для этого до- статочно добавить в конецобъявления команду присваивания: public int MagazineSize { get; private set; } = 16; Аесливыхотите, чтобыиграпозволялауказать количествошариковвмагазине?Вероятно, большинство экземпляров маркеровбудет создаваться в заряженном виде, но на уровнях повышенной сложности некоторые маркеры могут создаваться незаряженными, чтобы игрок вынужден был перезарядить их передстрельбой.Как это сделать? 1 2 3 Замените! В:Можно ещераз объяснить, что делает конструктор? О: Конструктор — метод, который вызывается при создании нового экземпляра класса. Он всегда объявляется как метод без возвращаемого типа, имя которого совпадает с именем класса. Чтобы понять, как работает конструктор, создайте новое консольное приложение идобавьте класс ConstructorTest с конструктором и открытым полем с именемi: public class ConstructorTest { public int i = 1; public ConstructorTest() { Console.WriteLine($"i is {i}"); } } Затем добавьте в методMain командуnew: new ConstructorTest(); Чтобы по-настоящему понять, как работают конструкторы, воспользуемся отладчиком. Добавьте три точки прерывания: • В объявлении поля(i = 1). • В первой строке конструктора. • Нафигурной скобке} за последней строкой методаMain. Отладчик сначала прерывается в объявлении поля, затем в конструкторе и, на- конец, в конце метода Main. Как видите, ничего загадочного в этом нет — CLR сначала инициализирует поля, затем выполняет конструктор и, наконец, продол- жает выполнение с той точки, где была остановка после команды new. сначала вызываются конструкторы часто Зад авае м ые вопросы
дальше 4 299 инкапсуляция Ой-ой,кажется,у нас проблема. Как только вы добавляете конструктор,IDE сообщает об ошибке вме- тодеMain: Каквыдумаете, что нужно сделатьдля исправления этойошибки? Использование конструктора с параметрами для инициализации свойств Ранеевэтой главеужебыло показано, что объект можно инициализировать в конструкторе — специ- альном методе,которыйвызывается присозданииэкземпляра.Конструкторы ничемнеотличаютсяот другихметодов,а следовательно,онимогут получать параметры. Конструкторыс параметрамиисполь- зуютсядля инициализации свойств. Конструктор, только что созданныйнамивразделе «Часто задаваемые вопросы», выглядиттак: public ConstructorText().Этоконструкторбез параметров, иегообъявление, какиобъявлениелюбого другого метода без параметров,завершаетсякруглымискобками().Добавим вклассPaintballGunконструктор без параметров. Этотконструктор выглядит так: public PaintballGu n(int balls, int mag azineS ize, bo ol load ed) { this.balls = ball s; Mag azineSi ze = ma gazine Size; if (!loaded) Relo ad(); } Конструктор выполняется сразу же после созда- ния нового экземпляра, поэтому мы включаем в тело метода код инициализации количества шариков и размера магазина и перезарядки маркера в случае необходимости. Обратите вни- мание на ключевое слово this в первой строке. Как вы думаете, для чего оно нужно? Добавьте конструктор в класс — создай- те метод, не имеющий возвращаемого типа, с таким же именем , как у класса. Конструктор получает три параметра: int с именем balls, int с именем magazineSizeи bool с именем loaded. Если имя параметра совпадает с именем поля, параметр замещает поле. Имя параметра конструктора balls совпадает с именем поляballs. При совпадении имен пара- метр обладает более высоким приоритетом в теле конструктора. Этот механизм называ- ется замещением: когда имя параметра или переменной в методе совпадает с именем поля, при использовании в методе это имя будет обозначать переменную или параметр, но не поле. Именно этим объ- ясняется необходимость ключевого словаthis в конструкторе PaintballGun: this.balls = balls; Когда мы используем имя balls, оно обозначает параметр. Мы хотим задать значение поля, и так как оно обладает таким же именем, для обращения к нему необходимо использоватьthis.balls. Кстати, это относится не только к конструкторам, но и к любым методам. Будьте осторожны!
300 глава 5 Передача аргументов при использовании ключевого слова "new" Когда вы добавили конструктор,IDE сообщила, что метод Main содержит ошибку в команде new (PaintballGun gun = new PaintballGun()).Ошибка выглядит так: Прочитайтетекст ошибки—в немточно описана суть проблемы.Ваш конструктор использует аргументы, поэтому ему должны передаваться параметры. Начните с повторного ввода команды new, и IDE точно скажет,что необходимо добавить: Мыиспользуем new для созданияэкземпляровклассов. До сих пор увсехнашихклассовбыликонструк- торыбезпараметров, поэтому передавать аргументыбыло не нужно. Теперьунас имеется конструктор с параметрами, и,каки любойметодс параметрами, онтребуетпере- дачиаргументов,типы которых соответствуюттипам параметров. Изменим метод Main так, чтобы он передавал параметры конструктору PaintballGun. Добавьте метод ReadInt, написанный в главе 4 для калькулятора характеристикОуэна. Аргументы конструкторанеобходимо откуда-то получить. Унасуже имеетсяидеально подходящий метод, которыйзапрашиваетупользователязначенияint, поэтомуегостоит использоватьповторно. Доба вьт е код для ч т ения да нных из консоль ного ввода . После добавления метода ReadInt из главы 4мы используем его для получения двух значенийint. Включите следующие четыре строки кода в начало метода Main: int numberOfBalls = ReadInt(20, "Number of balls"); int magazineSize = ReadInt(16, "Magazine size"); Console.Write($"Loaded [false]: "); bool.TryParse(Console.ReadLine(), out bool isLoaded); Обновите команду new и добавьте аргументы. Значенияхранятсяв переменных, тип которыхсоответствуеттипупараметров конструктора, имы можем обновить команду new, чтобыони передавались конструкторуваргументах: PaintballGun gun = new PaintballGun(numberOfBalls, magazineSize, isLoaded); Запустите программу. Запуститепрограмму.Она запрашиваетколичество шариков,размер магазина, а такжеисходное состояние маркера(заряжен или нет).Затем программа создает новый экземпляр PaintballGun, передавая его конструктору аргументы, соответствующие выбранным значениям. 1 2 3 4 Если метод TryParseне может разобрать содержи- мое строки, он оставляет isLoaded значение по умол- чанию (false для типаbool). Измените! передача аргументов конструктору
дальше 4 301 инкапсуляция class Q { public Q(bool add) { if (add) = "+"; else = "*"; N1= . ; N2= . ; } public Random R = new Random(); public N1 { get; set; } public Op { get; set; } public N2 { get; set; } public Check(int ) { if( == "+") return (a N1 + N2); else return (a * ); } } class Program { public static void Main(string[] args) { Q = Q( .R. == 1); while (true) { Console.Write($"{q. } {q. } {q. }="); if (!int.TryParse(Console.ReadLine(), out int i)) { Console.WriteLine("Thanks for playing!"); ; } if( . ( )){ Console.WriteLine("Right! "); = Q( .R. == 1); } else Console.WriteLine("Wrong! Try again."); } } } Внимание: каждый фрагмент можетисполь- зоваться много- к ра тно! if el se new return wh ile for fo rea ch Q add Main Op Random R N1 N2 out Next() Next(1, 10) Nex t( 2) Next(1, 9) Che ck class v oid int public private static add int ar gs bo ol string double float a b c i j k q r s + * - *= == += Мы слегка завысили сложность этого упражнения! Не забывайте: подсмотреть в решение — не значит жульничать! Программа задает серию вопро- сов со случайными операциями сложения иумножения ипроверя- етответы.Воткак она выглядитво время игры: 8+5=13 Right! 4*6=24 Right! 4*9=37 Wrong! Try again. 4*9=36 Right! 9*8=72 Right! 6+5=12 Wrong! Try again. 6+5=9 Wrong! Try again. 6+5=11 Right! 8*4=32 Right! 8+6=Bye Thanks for playing! У бассейна Ваша задача — выловить кусочки кода из бассей- на и расставить их в коде. Один фрагмент может ис- пользоваться многократно, использовать все фрагменты не обязательно. Ваша цель — создать классы, которые успешно компили- руются и работают и выдают результат, со- впадающий с приведенным примером. Игра заверша- ется при вводе любого ответа, который не яв- ляется числом. Игра генерирует случайные вопросы с операциями сло- жения и умножения. Если пользователь дал не- правильный ответ, про- грамма снова и снова за- дает вопрос, пока не будет получен правильный ответ.
302 глава 5 задача сложная, но она вам по силам! if el se new return while for fo re ach Q add Mai n Op Random R N1 N2 out class Q { public Q(bool add) { if (add) Op = "+"; else Op = "*"; N1= R . Next(1, 10) ; N2= R . Next(1, 10) ; } public static Random R = new Random(); public int N1 { get; private set; } public string Op { get; private set; } public int N2 { get; private set; } public bool Check(int a) { if ( Op == "+") return (a == N1 + N2); else return (a == N1*N2); } } class Program { public static void Main(string[] args) { Qq= new Q(Q.R. Next(2) ==1); while (true) { Console.Write($"{q. N1 } {q. Op } {q. N2 } = "); if (!int.TryParse(Console.ReadLine(), out int i)) { Console.WriteLine("Thanks for playing!"); return ; } if(q.Check(i)){ Console.WriteLine("Right! "); q=newQ(Q.R.Next(2) == 1); } else Console.WriteLine("Wrong! Try again."); } } } Программа задает серию вопро- сов со случайными операциями сложения и умножения и проверя- етответы.Воткакона выглядитво время игры: 8+5=13 Right! 4*6=24 Right! 4*9=37 Wrong! Try again. 4*9=36 Right! 9*8=72 Right! 6+5=12 Wrong! Try again. 6+5=9 Wrong! Try again. 6+5=11 Right! 8*4=32 Right! 8+6=Bye Thanks for playing! Next() Next(1, 10) Nex t( 2) Next(1, 9) Che ck class v oid int public private static add int ar gs bo ol string double float У бассейна. Решение Ваша задача — выловить кусочки кода из бассейна и расставить их в коде. Один фрагмент может исполь- зоваться многократно, использовать все фрагменты не обязательно. Ваша цель — создать классы, кото - рые успешно компилируются и работают и выдают результат, совпадающий с приведенным примером. Внимание: каж- дый фрагмент можетисполь- зоваться много- к ра тно! Игра заверша- ется при вводе люб ого отв е та, который не яв- ляется числом. Игра генерирует случайные вопросы с операциями сло- жения и умножения. Если пользователь дал неправильный от- вет, программа снова и снова задает вопрос, пока не будет получен правильный ответ. a b c i j k q r s + * - *= == += Мы пометили «галочкой» все фраг- менты, использованные в решении.
дальше 4 303 инкапсуляция Несколько полезных фактов о методах и свойствах Ì Каждый метод класса имеет уникальную сигнатуру. Первая строка метода, которая содержит модификатор доступа, возвращаемое зна- чение, имя и параметры, называется сигнатурой метода. Свойства тоже обладают сигнатурами — они состоят из модификатора доступа, типа и имени. Ì Свойства могут инициализироваться в инициализаторе объекта. Вы ужеиспользовали инициализаторы объектов: Guyjoe=newGuy(){Cash=50,Name= "Joe"}; Свойства также могут указываться в инициализаторе объекта. В таком случае сначала выполняется конструктор, а затем задаются значения свойств. В инициализаторе объ- екта могут инициализироваться только открытыеполя и свойства. Ì Конструктор есть у каждого класса, даже если вы не добавили его са мостоя те ль но. Конструктор необходим CLR для создания экземпляра объекта — он задействован во внутренних механизмах работы .NET. Таким образом, если вы не добавили конструк- тор в свой класс, компилятор C# автоматически добавит конструктор без параметров. Ì Чтобы предотвратить создание экземпляров класса из других классов, доба вьте при ват ный кон структ ор. В некоторых ситуациях процесс создания объектов должен тщательно контролиро- ваться. Один из способов заключается в том, чтобы объявить конструктор приват- ным —тогда он может вызываться только из этого класса. Попробуйтесами: class NoNew { private NoNew() { Console.WriteLine("I'm alive!"); } public static NoNew CreateInstance() { return new NoNew(); } } Добавьте класс NoNew в консольное приложение. Если вы попытаетесь включить команду new NoNew(); в метод Main, компилятор C# сообщает об ошибке ('NoNew. NoNew()' недоступен из-за уровня защиты), но метод NoNew.CreateInstance без ма- лейших проблем создает новый экземпляр.
304 глава 5 хорошие игры и эмоциональное воздействие Самое время поговорить об эстетике видеоигр. Если задуматься, инкапсуляция не дает воз- можности сделать что-то такое, чего вы не могли сделать до этого. Те же самые про- граммы можно написать без свойств, конструкторов и приватных методов — но они будут выглядеть иначе. Дело в том, что работа по программированию не всегда направлена на то, чтобы ваш код делал что-то новое. Часто программа должна делать то же самое, но другим способом. Помните об этом, когда будете читать об эстетике. Она не влияет на поведение вашей программы, она отражается на восприятии игрового процесса игроком. Эст ети ка Чтовы чувствовали, когда в последнийраз проводили время за игрой?Вамбыло интересно?Вы чувствовали волнение, приток адреналина? Возникало ли у вас ощущение открытия илидостижения?Вы конкурировали с другимиигроками или сотруднича- ли с ними? Был ли в игреувлекательный сюжет? Вам было весело? Грустно?Игры вызывают у нас эмоциональный отклик, и именно эта идея лежит в основе эстетики. Вас удивляютразговоры о чувствах в видеоиграх?Напрасно — эмоции и чувства всегда играли важную роль в проектировании игр, и у самых успешных игр всегда присутствовал важный эстетический аспект. Вспомните то чувство глубокого удовлетворе- ния, когда вы роняетедлинную «четверку» в Тетрисе и онаубирает четырерядаблоков. Или то волнение вPac-Man, когда вы подбираете энергетическую таблетку и поворачиваетесь к преследующим вас по пятам призракам. • Очевидно, что на эстетику игры может влиять художественный стиль и визуальное оформление, музыка и звук, сю - жет, но эстетика — нечто большее, чем художественные элементы игры. Эстетика может формироваться структуриро- ванием игры. • Эстетика присуща не только видеоиграм, она также присутствует в настольных играх. Покер знаменит своими пережива- ниями от взлетов и падений, от удачно провернутого блефа. Даже у такой простой игры, как Go Fish!, есть своя эстетика: «перетягивание каната» в процессе того, как игроки выясняют, какие карты нарукеу соперников; нарастающие эмоции, когда игрок выкладывает на стол очередную «книгу»;радость от того, что вам пришла самая нужная карта; облегчение, с которым вы произносите «GoFish!», когда противник требует у вас отсутствующую карту. • Иногда говорят о «геймплее», но при обсуждении эстетики стоит выражаться точнее. • Игра создает испытания для игрока. Она предоставляет ему препятствия, которые нужно преодолевать, чтобы почув- ствовать себя победителем. • Игровое повествование вовлекает игрока вдраматическую сюжетную линию. • Тактильные ощущения игры — игровой ритм, утробные звуки, с которыми вы подбираете энергетические таблетки, рас- катистый звук и размывка ускоряющегося автомобиля — все это работает на то, чтобы вы получилиудовольствие. • Кооперативные и многопользовательские игры создают чувство принадлежности к сообществу. • Игра с элементами фантастики не только переносит игрока вдругой мир, но и позволяет ему быть совершенно другим человеком (или вообще не человеком!). • Игры с элементами самовыражения помогают игроку лучше разобраться в себе, становятся способом самопознания. Хотите верьте, хотите нет, но идеи, лежащие в основе эстетики, помогут вам сделать более общие выводы о разработке, применимые не только к играм, но и к любым программам и приложениям. Не торопитесь идайте этим идеям закрепитьсяу вас в мозгу — мы вернемся к ним в следующей главе. Разработка игр... и не только Некоторые разработчики скептически относятся к обсуждениям эстетики — они считают , что важна только механика игры. Небольшой мысленный эксперимент покажет, насколько важ- ной может быть эстетика. Допустим, имеются две игры с идентичной механикой. Между ними есть только одно крошечное различие: в одной игре вы пинаете булыжники, чтобы предотвра- тить лавину и спасти деревню. В другой игре вы пинаете щенков и котят просто потому, что вы ужасный человек. Даже если все остальные аспекты игры идентичны, это будут две совер- шенно разные игры. Такова сила эстетики!
дальше 4 305 инкапсуляция В этом коде есть проблемы. Предполагается, что он управляет работой простого автомата по продаже жевательной резинки: вы бросаете монетку, автомат выдает жвачку. Мы обнаружили в программе четыре проблемы, из-за которых возникают ошибки. Напишите,что, по-вашему, нетакв строках,на которые указываютстрелки. cla ss Gumb allMac hine { privat e int gumball s; pri vate int price; public int Price { get { retu rn pric e; } } public Gum ballMac hine(int gumballs, i nt pric e) { gu mballs = this. gumball s; price = Price; } public string Dispen seOneGu mball( int price, int coins Inserted) { // Провер ка резервного поля p rice if (this. coinsIn serted >= pri ce) { gumballs -= 1; ret urn "He re’s yo ur gum ball"; }else{ ret urn "In sert mo re coi ns"; } } } Возьми в руку карандаш
306 глава 5 исп рав лен ие к ласса оуэ на В этом коде есть проблемы.Мы обнаружилив программе четыре пробле- мы,из-за которыхвозникают ошибки.Ниже приведено ихописание. public GumballMachine(int gumballs, int price) { gumballs = this.gumballs; price = Price; } public string DispenseOneGumball(int price, int coinsInserted) { // Проверка резервного поля price if (this.coinsInserted >= price) { gumballs -= 1; return "Here's your gumball"; }else{ return "Insert more coins"; } } Ключевое слово this использует- ся с параметром, где ему не место. Оно должно исполь- зоваться с price, потому что это поле замещается параметром. Этот параметр за- мещает приватное поле с именем price, а в комментарии го- ворится, что метод должен проверять значение резервного поля price. Ключевое слово «this» применяет- ся не там, где нужно. Выражение this. gumballs относится к свойству, а имя gumballs относится к параметру. Имя price, начинающееся с буквы нижнего реги- стра, относится к параметру конструктора, а не к полю. Эта строка присваивает ПАРАМЕ- ТРУ значение, возвращенное get-методом Price, но значение Price еще не задано, так что ничего полезного этот вызов не сделает. Если поменять местами операнды (Price = price), команда будет работать. В: Если конструктор является методом, то почему у него нет возвращаемого типа? О:Конструктор неимеет возвращаемого типа,потому чтокаждый конструктор всегдавозвращает void.Былобы избыточнозаставлять разработчика вводить void в начале каждого конструктора. В: Может лисвойство иметь get-метод без set-метода? О: Да! Создавая свойство, для которого определен толькоget- метод, вы определяете свойство, доступное только для чтения. Например, класс SecretAgentможет содержать открытоесвойство, доступное толькодля чтения, с резервным полем: string spyNumber = "007"; public string SpyNumber { get { return spyNumber; } } В: И наверняка свойство может иметь set-метод без get- метода? О: Да, если только свойство не является автоматическим. В та- ком случае компилятор сообщает об ошибке («Автоматически реализуемые свойствадолжны иметь get-методы доступа»).Если вы создаете свойство с set-методом, но без get-метода, то это свойство будет доступно толькодля записи. Класс SecretAgent может использовать его длясозданиясвойства,значениякоторого другиеэкземпляры смогут присвоить, но не смогутузнать: public string DeadDrop { set { StoreSecret(value); } } Оба варианта — set-методбез get-метода или наоборот — могут быть чрезвычайно полезными для целей инкапсуляции. Атеперь самое время выделить еще несколько минут и очень внимательно проанализировать этот код.Это распространенные ошибки, которые встречаютсяу многих новичков при работе с объектами. Если вы научи- тесь их избегать, программирование станет намного более приятным. Возьми в руку карандаш Решение часто Задава ем ые вопросы
дальше 4 307 инкапсуляция Применитето, что вы узнали выше об инкапсуляции, в калькуляторе поврежденийОуэна. Измените класс SwordDamage, чтобы заменить поля свойствами, и добавьте конструктор.Когда этобудет сделано, обно- вите консольное приложение, чтобы в нем использовалась новая версия класса. Наконец, исправьте при- ложение WPF. (Чтобы вам было проще, создайте новое консольное приложение для первых двух частей и новое приложение WPF для третьей части). Часть 1: Измените SwordDamage, чтобы классбыл хорошо инкапсулированным 1. Удалите полеRoll и замените его свойством с именемRoll ирезервным полем с именем roll. Get-метод возвращает значе- ние резервного поля. Set-метод обновляет резервное поле, а затем вызывает метод CalculateDamage. 2. Удалите метод SetFlaming и замените его свойством с именемFlaming и резервным полем с именем flaming.Новое свой- ство должно работать так же, как Roll, — get-метод возвращает значение резервного поля, set-метод обновляет его и вы- зывает методCalculateDamage. 3. Удалите методSetMagic и замените его свойством с именем Magic ирезервным полем с именем magic, которое работает точно так же, как свойстваFlaming и Roll. 4. Создайте автоматически реализуемое свойство с именем Damagе. Свойство должно иметь открытый get-метод и при- ватный set-метод. 5. Удалите поляMagicMultiplier и FlamingDamage. Измените метод CalculateDamage так, чтобы он проверял значения свойств Roll, Magic и Flaming и выполнял все вычисления внутри метода. 6. Добавьте конструктор, который получает исходныйрезультатброска в параметре. Так как метод CalculateDamage вызыва- ется только из set-методов свойств и конструктора, вызывать его из другого классане нужно. Объявите метод приватным. 7. Добавьтедокументацию XML во все открытые компоненты класса. Часть 2: Измените консольное приложение, чтобы в нем использовался хорошо инкапсулированный класс SwordDamage 1. Создайте статический метод с именем RollDice, который возвращает результат броска 3d6. Экземпляр Random должен храниться в статическом поле вместо переменной, чтобы он мог использоваться как методомMain, так и методомRollDice. 2. Вызовите новый методRollDice, чтобы задать значения свойстваRoll и аргумента конструктора SwordDamage. 3. Измените код с вызовами SetMagic иSetFlaming, чтобы вместо методов в нем использовались свойства Magic иFlaming. Часть 3: Измените приложениеWPF, чтобы в нем использовался хорошо инкапсулированный класс SwordDamage 1. Скопируйте код из части 1 в новое приложение WPF.Скопируйте кодXAML из проекта, приведенногоранее в этой главе. 2. В коде программной части объявите полеMainWindow.swordDamage(и создайте его экземпляр в конструкторе): Sw ordD amage swo rdDam age; 3. В конструктореMainWindow присвойте полю swordDamage новый экземпляр SwordDamage, инициализированный случай- ным броском3d6. Затем вызовите метод CalculateDamage. 4. Методы RollDice и Button_Click работают точно так же, как было показаноранее в этой главе. 5. Измените метод DisplayDamage, чтобы в нем использовалась строковая интерполяция. Метод должен выводить ту же строку. 6. Измените обработчики события Checked иUnchecked, чтобы оба флажка использовали свойстваMagic иFlaming вместо старых методовSetMagic и SetFlaming, а затем вызовите DisplayDamage. Протестируйте весь код.Воспользуйтесь отладчиком или командой выводаDebug.WriteLine и убедитесь в том, что он ДЕЙСТВИТЕЛЬНО работает. Упражнение Версия этого проекта для Mac доступна в приложении «VisualStudioдля пользователейMac».
308 глава 5 ответ к упражнению Теперь у Оуэна появился новый класс для вычисления повреждений. Пользоваться им намного проще, а риск ошибок снижается. Каждое свойство вычисляет повреждения за- ново, поэтому порядок вызова роли не играет. Ниже приведен код хорошо инкапсулиро- ванного класса SwordDamage. class SwordDamage { private const int BASE_DAMAGE = 3; private const int FLAME_DAMAGE = 2; /// <summary> /// Contains the calculated damage. /// </summary> public int Damage { get; private set; } private int roll; /// <summary> /// Sets or gets the 3d6 roll. /// </summary> public int Roll { get { return roll; } set { roll = value; CalculateDamage(); } } private bool magic; /// <summary> /// True, если меч волшебный; false в противном случае. /// </summary> public bool Magic { get { return magic; } set { magic = value; CalculateDamage(); } } private bool flaming; /// <summary> /// True, если меч огненный; false в противном случае. /// </summary> public bool Flaming { get { return flaming; } set { flaming = value; CalculateDamage(); } } Свойство Roll с приватным резервным полем. Set- метод доступа вызывает метод CalculateDamage, который автоматически обновляет свойство Damagе. Приватный set-метод доступа свойства Damage делает его доступным только для чтения, поэтому оно не может быть перезаписано другим классом. Свойства Magic и Flaming работают так же, как свойство Roll. Все они вы- зывают CalculateDamage, так что при присваивании значения любому из них ав- томатически обновляется свойство Damage. Так как эти константы не будут ис- пользоваться другими классами, будет разумно объявить их приватными. Упражнение Решение
дальше 4 309 инкапсуляция /// <summary> /// Вычисляет повреждения в зависимости от текущих значений свойств. /// </summary> private void CalculateDamage() { decimal magicMultiplier = 1M; if (Magic) magicMultiplier = 1.75M; Damage = BASE_DAMAGE; Damage = (int)(Roll * magicMultiplier) + BASE_DAMAGE; if (Flaming) Damage += FLAME _DAMAGE; } /// <summary> /// Конструктор вычисляет повреждения для значений Magic и Flaming по умолчанию /// и начального броска 3d6. /// </summary> /// <param name="startingRoll">Начальный бросок 3d6</param> public SwordDamage(int startingRoll) { roll = startingRoll; CalculateDamage(); } } Код метода Main консольного приложения: class Program { static Random random = new Random(); static void Main(string[] args) { SwordDamage swordDamage = new SwordDamage(RollDice()); while (true) { Console.Write("0 for no magic/flaming, 1 for magic, 2 for flaming, " + "3 for both, anything else to quit: "); char key = Console.ReadKey().KeyChar; if (key != '0' && key != '1' && key != '2' && key != '3') return; swordDamage.Roll = RollDice(); swordDamage.Magic = (key == '1' || key == '3'); swordDamage.Flaming = (key == '2' || key == '3'); Console.WriteLine($"\nRolled {swordDamage.Roll} for {swordDamage.Damage} HP\n"); } } private static int RollDice() { return random.Next(1, 7) + random.Next(1, 7) + random.Next(1, 7); } } Все вычисления инкапсулируются внутри метода CalculateDamage. Все зависит только от get- методов доступа для свойств Roll, Magic и Flaming. Конструктор задает значение ре- зервного поля для свойства Roll, по- сле чего вызывает CalculateDamage, чтобы обеспечить правильность значения Damage. Будет разумно выделить бросок 3d6 в отдельный ме- тод, потому что он вызывается из двух разных точек Main. Если вы воспользуетесь для его создания командой «Generate Method», IDE объявит его приватным автома- тически. Упражнение Решение
310 глава 5 ответ к упражнению Ниже приведен код программной части для настольного приложенияWPF.Код XAML остается таким же, как прежде. Мы не предлагали вам переместить бросок 3d6 в отдельный метод. Как вы думаете, добавление метода RollDice (как в консольном приложении)упростит чтение этого кода? Или этот метод лишний? Нельзя сказать, что один ответ однозначно лучше или хуже другого. Опробуйте оба варианта иреши- те, какой из них вам лучше подходит. public partial class MainWindow : Window { Random random = new Random(); SwordDamage swordDamage; public MainWindow() { InitializeComponent(); swordDamage = new SwordDamage(random.Next(1, 7) + random.Next(1, 7) + random.Next(1, 7)); DisplayDamage(); } public void RollDice() { swordDamage.Roll = random.Next(1, 7) + random.Next(1, 7) + random.Next(1, 7); DisplayDamage(); } void DisplayDamage() { damage.Text = $"Rolled {swordDamage.Roll} for {swordDamage.Damage} HP"; } private void Button_Click(object sender, RoutedEventArgs e) { RollDice(); } private void Flaming_Checked(object sender, RoutedEventArgs e) { swordDamage.Flaming = true; DisplayDamage(); } private void Flaming_Unchecked(object sender, RoutedEventArgs e) { swordDamage.Flaming = false; DisplayDamage(); } private void Magic_Checked(object sender, RoutedEventArgs e) { swordDamage.Magic = true; DisplayDamage(); } private void Magic_Unchecked(object sender, RoutedEventArgs e) { swordDamage.Magic = false; DisplayDamage(); } } Принятие решения о том, стоит ли выделить одну строку дублиру- ющегося кода в отдельный ме- тод, — хороший пример эстетики в коде. Красота в глазах смотря- ще го. Упражнение Решение
дальше 4 311 инкапсуляция ¢ Инкапсуляция повышает безопасность кода, предот- вращая возможность некорректных изменений классов или иных злоупотреблений компонентами классов. ¢ Поля, требующие дополнительной обработки или вы- числений, становятся основными кандидатами для инкапсуляции. ¢ Подумайте, как можно некорректно использовать поля и методы ваших классов. Объявляйте поля и ме- тоды открытыми только при необходимости. ¢ Использование единой схемырегистра символов в име- нах полей, свойств, переменных и методов упрощает чтение кода. Многие разработчики используют схему «верблюжьего регистра» (camelCase) для приватных полей или схему «регистра Pascal» (PascalCase) для открытых полей. ¢ Свойство является компонентом класса, который вы- глядит как поле при использовании, но при выполнении работает как метод. ¢ Определение get-метода состоит из ключевого сло- ва get и метода, возвращающего значение свойства. ¢ Определение set-метода состоит из ключевого слова set и метода, присваивающего значение свойства. Внут- риметода ключевое слово value определяет перемен- ную, доступную только для чтения, которая содержит присваиваемое значение. ¢ Свойства часто читают или присваивают значение ре- зервного поля, т . е . приватного поля, которое инкап- сулируется за счет ограничения доступа к нему через свойство. ¢ Автоматически реализуемое свойство, иногда на- зываемое автоматическим свойством, имеет get-метод, возвращающий значениерезервного поля, и set-метод дляего обновления. ¢ Используйте фрагменты кода в Visual Studio для соз- дания автоматически реализуемых свойств; для этого введите «prop» и дважды нажмите клавишуTab. ¢ Используйте ключевое словоprivate для ограничения доступа к get- или set-методу. Свойство, доступное только для чтения, имеет приватный set-метод. ¢ При создании объекта CLR сначала задает значения всех полей, инициализируемых при объявлении, вы- полняет конструктор и возвращается к команде new, создавшей объект. ¢ Используйте конструктор с параметрамидля инициа- лизации свойств. Аргументы, передаваемые конструкто- ру, задаются при использовании ключевого слова new. ¢ Параметр, имя которого совпадает с именем поля, за- мещает это поле. Используйте ключевое слово this дляобращения к полю. ¢ Если вы не включите конструктор в свой класс, компи- лятор C# автоматически добавляет конструктор без п а раме тров. ¢ Чтобызапретить создание экземпляров издругихклас- сов, добавьте приватный конструктор. КЛЮЧЕВЫЕ МОМЕНТЫ
Наследование 6 Генеалогическое древо объектов Входя в крутой поворот, я вдругпонял, что унаследовал свой велосипед от ДвухКолесного, но забыл про метод Тормоза()... В итоге двадцать шесть швов и лишение прогулок на целый месяц. Иногда люди ХОТЯТ быть похожими на своих родителей. Вы встречали объект, который действует почти так, как нужно? Думали ли вы о том, что при изме- нении всего нескольких элементов класс стал бы идеальным? Наследование позволяет расширять существующие классы, чтобы новый класс получал все поведение существующего, сохраняя при этом гибкость для внесения изменений, чтобы его можно было адаптировать под любые конкрет- ные требования. Наследование является одним из самых мощных инструментов C#: в частности, оно помогает избегать дублирования кода, более адекватно моделировать реальный мир и в конечном итоге упрощает сопровождение и снижает риск ошибок.
314 глава 6 добавление нового оружия Вычисление повреждений для ДРУГИХ видов оружия Обновленный калькулятор повреждений от меча стал настоящим хитом! ТеперьОуэнхочетиметь калькуляторы длявсехвидоворужия.Начнем с вы- числения повреждений для стрелы, которая используетбросок1d6.Созда- дим новыйклассArrowDamage,которыйбудетвычислять повреждения от стрелы поформулеизблокнота гейм-мастераОуэна. Большая часть кода ArrowDamage будет идентична коду класса SwordDamage. Чтобыначатьпостроение новогоприложения, выполните следующие действия: Создайте новый проект консольного приложения .NET. Так как вычисления должны выполняться как для мечей, так и для стрел,до- бавьтеклассSwordDamageв проект. Создайте класс ArrowDamage, который является точной копией SwordDamage. Создайте новый класс с именем ArrowDamage, затем скопируйте весь код из SwordDamage и вставьте его в новый класс ArrowDamage. Затем замените имя конструктора на ArrowDamage, чтобы программа нормально строилась. Проведите рефакторинг констант. Формула повреждений от стрелы ис- пользуетдругиезначения для базовыхи огненныхповреждений,поэтому переименуйте константу BASE_DAMAGE в BASE_MULTIPLIER и обнови- те значения констант. Мы считаем, что эти константы упрощают чтение кода, поэтому также добавьте константу MAGIC_MULTIPLIER: private const decimal BASE_MULTIPLIER = 0.35M; private const decimal MAGIC_MULTIPLIER = 2.5M; private const decimal FLAME_DAMAGE = 1.25M; Измените метод CalculateDamage. Чтобы новый класс ArrowDamage заработал, осталось сделать последний шаг: обно- вить метод CalculateDamage, чтобы он выполнял правильные вы чи сл ения: private void CalculateDamage() { decimal baseDamage = Roll * BASE_MULTIPLIER; if (Magic) baseDamage *= MAGIC_MULTIPLIER; if (Flaming) Damage = (int)Math.Ceiling(baseDamage + FLAME_DAMAGE); else Damage = (int) Math.Ceiling(baseDamage); } 1 2 3 4 Вы согласны с тем, что с этими константами код лучше читается? А впрочем, если не согласны — это нормально! * базовые повреждения от Стрелы равны резуль- тату броСка 1d6, умно- женному на 0.35 HP. * для волшебной Стрелы базовые повреждения умножаютСя на 2.5 HP. * огненная Стрела до- бавляет еще 1.25 HP. * результат округляетСя в большую Сторону до б лижай ше го цело го. ArrowDamage Roll Magic Flaming Damage Метод Math.Ceiling может использоваться для округле- ния значений в большую сто- рону. Тип при этом сохраня- ется, так что значение нужно будет привести к типу int. Код для выполнения нек оторой операции можно написать разными спо- собами. Сможете ли вы предложить другой способ написания кода, вы- числяющего повреждения от стрелы? Мозговой штурм Сделайте это!
дальше 4 315 н аслед ов ание Команды switch для выбора из нескольких кандидатов Обновимконсольноеприложение,чтобы оно запрашивалоу пользователя, для какого оружиядолжны вычислятьсяповреждения— длямеча илидлястрелы. Программа предлагаетнажать клавишу иисполь- зует статический метод Char.ToUpper для преобразования к верхнему регистру: Console.Write("\nS for sword, A for arrow, anything else to quit: "); weaponKey = Char.ToUpper(Console.ReadKey().KeyChar); Теоретически для этой цели можно воспользоваться набором команд if/else: if (weaponKey == 'S') { /* вычисление повреждений от меча */ } else if (weaponKey == 'A') { /* вычисление повреждений от стрелы */ } else return; Такмыобрабатывали вводимыеданные донастоящего момента. Сравнениеодной переменной сомногими разнымизначениями — довольно распространенный паттерн,который встречается снова и снова. Он встречается настолько часто,чтоC#содержит специальнуюразновидность команды,предназначенную конкретнодля этой ситуации.Команда switch позволяетсравнить однупеременную сомногимизначени- ями в компактном простом синтаксисе. Следующая команда switch делает абсолютно то же самое, что делаютприведенныевышекоманды if/else: sw itch (weaponKey) { case 'S': /* вычис лить повреждения от меча */ break; case 'A': /* вычис лить повреждения от стрелы */ break; default: return; } Метод Char.ToUpper преобразует 's' и 'a' в'S'и'A'. Сначала идет ключевое слово switch, за ко- торым следует то, что должно сравниваться с разными возможными значениями. Тело команды switch представляет собой на- бор секций, в которых пр ов еряем ое знач ение сравнивается с конкрет- ными вариантами. Каж- дая проверка начинается с ключевого слова case, конкретного проверяемо- го значения и двоеточия и завершается командой br eak. Ключевое слово default можно сравнить с последней командой else в конце серии команд if/else. Эта секция выполняется в том случае, если ни один из предыдущих вариантов не подошел. Обновите метод Main так, чтобы в нем использовалась команда switchдлявыбора типа оружия. Для начала скопируйте методы Main и RollDice из ответа к упражнению в конце предыдущей главы. 1. Создайте экземплярArrowDamage в начале метода, сразу же после создания экземпляраSwordDamage. 2. Измените метод RollDice, чтобы он получал параметр int с именем numberOfRolls. Таким образом, при вызове RollDice(3) будет моделироваться бросок 3d6 (т. е. программа вызывает random.Next(1.7) три раза и суммирует результаты), а при вызове RollDice(1) будет моделироватьсябросок 1d6. 3. Добавьте две строки кода так, как они приведены выше, прочитайте ввод методомConsole.ReadKey, преобразуйте символ к верхнему региструChar.ToUpper и сохраните его в weaponKey. 4. Добавьте команду switch.Онадолжна полностью совпадать с командой switch, приведенной выше, кроме того что каждый из комментариев /* комментарий */ необходимо заменить кодом, который вычисляет поврежде- ния и выводит строку выходных данных на консоль. Упражнение
316 глава 6 что? ещебольше оружия?! Мытолько что познакомиливасссовершенно новым синтаксисом C# — командой switch — и пред- ложили использовать его в программе. Группа разработки C# в Microsoft постоянно совершенствует язык, и интеграция новых элементов языка в код относится к числу важных навыков C#. class Program { static Random random = new Random(); static void Main(string[] args) { SwordDamage swordDamage = new SwordDamage(RollDice(3)); ArrowDamage arrowDamage = new ArrowDamage(RollDice(1)); while (true) { Console.Write("0 for no magic/flaming, 1 for magic, 2 for flaming, " + "3 for both, anything else to quit: "); char key = Console.ReadKey().KeyChar; if (key != '0' && key != '1' && key != '2' && key != '3') return; Console.Write("\nS for sword, A for arrow, anything else to quit: "); char weaponKey = Char.ToUpper(Console.ReadKey().KeyChar); switch (weaponKey) { case 'S ': swordDamage.Roll = RollDice(3); swordDamage.Magic = (key == '1' || key == '3'); swordDamage.Flaming = (key == '2' || key == '3'); Console.WriteLine( $"\nRolled {swordDamage.Roll} for {swordDamage.Damage} HP\n"); break; case 'A ': arrowDamage.Roll = RollDice(1); arrowDamage.Magic = (key == '1 ' || key == '3 '); arrowDamage.Flaming = (key == '2 ' || key == '3 '); Console.WriteLine( $"\nRolled {arrowDamage.Roll} for {arrowDamage.Damage} HP\n"); break; default: return; } } } private static int RollDice(int numberOfRolls) { int total = 0; for (int i = 0; i < numberOfRolls; i++) total += random.Next(1, 7); return total; } } Создание экземпляра только что созданного класса ArrowDamage. Код, использующий экземпляр ArrowDamageдля вычис- ления повреждений, очень похож на код SwordDamage. Более того, они почти идентичны. Можно ли как-то сокра- тить дублирование кода и упростить чтение программы? Этот блок кода почти иденти- чен программе из последней главы. Просто на этот раз он не использует- ся в блоке if/else, а был преобразо- ван в секцию case команды switch (кроме того, в нем передается аргумент при вызове RollDice). Попробуйте сами! Установите точку прерывания в команде switch (weaponKey), после чего воспользуйтесь отладчиком для пошагового выполнения команды switch. Это по- может вам лучше разобраться в происходящем. Затем попробуйтеудалить одну из строк break — выполнение продолжится в следующей секции case (это называется сквозной передачей управления). Упражнение Решение
дальше 4 317 н аслед ов ание И еще... Можно ли вычислять повреждения от кинжала? От булавы? И шеста? И... Мысоздали два класса для вычисления повреждений отстрелы и меча. Но что, если в игре есть еще три вида оружия?Или четыре?Или двенадцать? А есливам придется заниматься сопровождением этого кода и позднее по- требуется вносить новыеизменения?Что, еслиодно итожеизменениенужно будет вносить в пяти или шести тесно связанных классах? Что, если изменения продолжатся?Ошибкистановятсяфактическинеизбежными — очень легко обновить пять классов, но забыть о шестом. Ого, мне приходится писать один и тот же код снова и снова. Я РАБОТАЮ КРАЙНЕ НЕЭФФЕКТИВНО. Должен быть более эффективный способ. Ве рно! П овто рение одного кода в нес кол ьких классах неэффективно и ненадежно. Ксчастью, C#предоставляет болееэффективныйспособ построенияклассов,которыесвязаныдругс другомиоб- ладаютобщим поведением: наследование. SwordDamage Roll Magic Flaming Damage ArrowDamage Roll Magic Flaming Damage CrossbowDamage Roll Magic Flaming Damage DaggerDamage Roll Magic Flaming Damage MaceDamage Roll Magic Spiked Damage WhipDamage Roll Magic Flaming Damage StaffDamage Roll Magic Damage А если некоторые классы имеют много общего, но не совсем идентичны? Что, если булава может быть шипастой или обычной, но огненной быть не может? Или если шест не может обладать ни одной из этих характе- ристик?
318 глава 6 нет смысла использовать золото там, где сгодится все, что блестит Если в ваших классах используется наследование, код достаточно написать только один раз То, чтовашиклассыSwordDamageиArrowDamage содержатмногосовпадающегокода, — не случайность. ПринаписаниипрограммC# вы часто создаетеклассы,представляю- щие реальные сущности,и эти сущности обычно связаны другс другом. Ваши классы содержат похожий код, потомучто сущности,которыеони представляютв реальном мире(два сходныхвычисления вролевойигре),обладают похожим поведением. Классы SwordDamage и ArrowDamage почти идентичны, потому что повреж- дения для обоих классов должны вы- числяться практически одинаково. SwordDamage Roll Magic Flaming Damage ArrowDamage Roll Magic Flaming Damage КлассыSwordDamage иArrowDamage почти идентичны, потому что в обоих случаях вычисленияпроизводятся по похожейсхеме. WeaponDamage Roll Magic Flaming Damage SwordDamage non-public method: CalculateDamage Эта стрелка на диаграмме классов оз начает, что к л асс SwordDamage на- следует от класса W eapo nD amage. ArrowDamage non-public method: CalculateDamage Оба класса наследуют все свои свойства от базового класса. Про- сто им нужны разные реализации метода C alcu lat eD amage. Способы вычисления повреждений в классах мечей и стрел похожи, но все же не совсем. При этом спо- собы управления их свойствами идентичны. Такой код можно разбить так, чтобы одинаковая часть находи- лась в базовом классе, а различаю- щиеся части — в двух субклассах.
дальше 4 319 н аслед ов ание Постройте модель классов: начните с общего и переходите к конкретике Общ ие Общ ие Конкретные Конкретные Food Dairy Product Cheese Che ddar Aged Vermont Cheddar Animal Bird Songbird Mockingbird Northern Mockingbird Когда вы строитенабор классов, представляющих разные сущности (особенно сущности из реального мира),результатом вашейработы станетмодельклассов. Сущностииз реальногомира часто образуют иерархию,котораяидет от общегок конкретному,иввашихпрограммахсоздаютсяиерархииклассов, которые делают то же самое. В модели классов классы, находящиеся на низких уровняхиерархии,на- следуют от классов более высоких уровней. К а ж дая птица — ж ивотное, но не ка ж дое ж ив отное — птица. В модели классов класс Cheese (Сыр) может наследовать от DairyProduct (Молочный продукт), который, в свою очередь, наследует от Food (Продукт питания). Для того, кто хочет взять домаш- него питомца, подойдетлюбая певчая птичка. Для орнитолога, изучающего семейство Mimidae, путать североамериканского пере- смешника (northern mockingbird) с южноамериканским (southern mockingbird) было бы непрости- тел ьн о. Класс на нижнем уровне иерархии наследует все (или почти все) атрибуты от всех классов, находящихся выше него. Все животные едят и размножаются, поэтому и североамерикан- ские пересмешники едят и размножаются. Если для вашего рецепта ну- жен сыр «чеддер» (Cheddar), вы можете использовать выдержан- ный вермонтский чеддер (Aged Vermont Cheddar). Если требуется именно выдержанный вермонт- ский чеддер, то любой чеддер не подойдет. Нужен только этот конкретный сыр. на-сле -до-вать, гл. Получать атрибут от родителя или предков.
320 глава 6 добро пожаловать в зоопарк Как бы вы спроектировали симулятор зоопарка? Львы,тигры и медведи... голова идет кругом!А также гиппопотамы, волки и даже собаки. Вам поручено спроектировать приложение, которое моделируетзоопарк. (Только неувлекайтесь — мы не будем строить код, а ограничимся классами, представляющими животных. Авыуженаверняка подумали,как это будетделаться в Unity!) Выполучилисписок некоторыхживотных, которыебудутзадейство- ванывпрограмме(но не всех!).Мы знаем, чтокаждое животное будет представленообъектом, аобъектыбудутперемещатьсяв рамкахмодели иделать то,что запрограммировано делать каждоеживотное. Ночтоеще важнее, программа должнабыть простой в сопровождении для другихпрограммистов; это означает,что они должны иметь воз- можность добавить своиклассы позднее,если потребуетсявключить всимулятор новыйвидживотных. Начнемспостроениямоделиклассовдля животных,которыеуже известны. Каким же будет первый шаг?Прежде чем говорить о конкретных животных, необходимо вычислить то общее, что есть у каждого животного, — абстрактные характеристики, которыми обладают все животные. Затемнаосновании этиххарактеристикбудет сформирован базовый класс, откоторого могутнаследовать все классы животных. Постарайтесь выявить характеристики, общие для всех живот ных . Присмотритесь кшестиживотным изсписка.Чтообщего ульва, гиппопотама, тигра, рыси, волка и собаки?Как они связаны? Необходимо выявить существующие между ними связи, чтобы сформировать модель классов, включающую всех животных. 1 Термины «родитель», «суперкласс» и «базовый класс» часто используют- ся как синонимы. Кроме того, термины «расши- рять» и «наследовать от» означают одно и то же. Термины «дочерний класс» и «субкласс» тоже являются синонимами, но «субкласс» также исполь- зуется в глаголе «субклас- с ирова т ь». Некоторые разработчики называют «базовым клас- сом» класс, находящийся на вершине иерархии на- следования, но... не на СА- МОЙ вершине, потому что каждый класс в конечном итоге наследует от Object или субкласса Object. Симулятор зоопарка включает сторожевую собаку, которая обхо- дит территорию парка и охраняет животных.
дальше 4 321 н аслед ов ание Animal Pi c ture Food Hunger Bo undari es Loca tion Mak eNoi se Eat Sl eep Roam Таккакдублирующийся кодсложно редактировать иеще сложнее читать, выберемметоды иполя для базового классаAnimal, которыебудут написаны только одинраз и которыебудутунаследо- ваны всеми производнымиклассами.Начнем с полей общего доступа: Ì Picture: картинка, которую можно поместить вPictureBox. Ì Food: тип пищи. Покау этого поля только два значения: meat (мясо) иgrass (трава). Ì Hunger:переменнаятипаint, показывающая, насколько животное хочет есть.Она меняется взависимостиотколичества выданного корма. Ì Boundaries: ссылка на класс,в котором хранится информацияо высоте, длинеи располо- жении вольера. Ì Location:координаты X иY, описывающиеместоположение животного. Построение базового класса для общих характеристик всех животных. Поля, свойства и методы базового класса дадут всем животным возможность наследовать общее состояние иповедение. Логично,что этоткласс долженназываться Animal (Животное). 2 Можно было сделать и другой выбор. На- пример, написать класс ZooOccupant, учитывающий рас- ходы на содержание животных, или класс Attraction, по- казывающий при- влекательность для посетителей. Но мы остановились на классе Animal. Вы согласны? Lion Hippo Tiger Dog Bobcat Wol f Крометого,в классе Animal присутствуютчетыре метода, которыемогутбыть унаследованы: Ì MakeNoise(): метод,позволяющийиздавать звуки. Ì Eat(): поведение при получении предпочитаемого корма. Ì Sleep():метод, заставляющий животноеспать. Ì Roam(): метод, учитывающий перемещения по во- льеру.
322 глава 6 внимание: разработчиков не кормить! Animal Picture F ood H unger B oun dari es Lo cati on MakeNoise Eat Sleep R oam У разных животных разное поведение Львы рычат, собаки лают, а бегемоты, насколько мы знаем, вообще не издают конкретных звуков. Каж- дый класс, производный отAnimal,унаследует метод MakeNoise(),но коды этихметодовбудутразличаться. Когда производный класс меняет поведение унаследо- ванного метода, говорято перекрытии(override). Трава — это вкусно! Я бы не прочь умять еще одну охапку сена. Без меня, пожалуйста. Определите, что каждое животное делает не так, как базовый класс Animal, или не делает вообще. Каждое животное должно есть, но собака может питаться маленькими кусочками мяса, а гиппопотам — огромными охапками сена. Какбудет выглядеть код для такого поведения? И собака, и гиппопотам переопределяют метод Eat. У гиппопотама пере- определеннаяверсиябудет поглощать, скажем,10 килограммов сенапри каждом вызове. Сдругойстороны, у собаки переопределенная реализация Eat будет уменьшать запас продуктовв зоопаркена одну 400-граммовуюбанку собачьих консервов. 3 Даже если свойство или ме- тод принадлежат базовому классу Animal, это не значит, что все субклассы должны использовать их одинаково... и вообще использовать хоть как- то! Итак, если у вас име- ется субкласс, насле - дующий от базового класса, он должен на- следовать все поведение базового класса... Но вы можете изменить его в субклассе, чтобы методы не работали аб сол ютно од ин ако- во. Собственно, в этом и заключается суть переопределения. Мы уже знаем, что некоторые животные переопределяют методы MakeNoise и Eat. Какие животные будут переопреде- лять методы Sleep(Спать) илиRoam(Бродить)?И будутли? Мозговой штурм
дальше 4 323 н аслед ов ание Определите, какие классы имеют много общего. Усобаки волков много общего,выне находите? Обавида относятсяксемейству собачьих, иможно довольно уверенноутверждать, что в их поведении тоже найдется немало сходства. Вероятно, они едят похожую еду и спят прибли- зительно водном режиме. А какнасчетрысей,тигров и львов?Оказывается, все три вида животных перемещаются в своей среде обитания практически одинаково. Можно сбольшойуверенностьюутверждать,что виерархию клас- сов можно включить общий классFeline, который существует между Animal иэтимитремяразновидностямикошачьих; дополнительный класс помогает предотвратить дублированиекода. 4 Animal Picture F ood Hu nger B ounda ri es Loc ati on MakeNoise Eat Sleep Ro am Hippo MakeNoise Eat Swi m Tiger MakeNoise Eat Dog MakeNoise Eat Bobcat MakeNoise Eat Wol f MakeNoise Eat Скорее всего, так- же можно доба- вить класс Canine, от которого наследуют как со- баки, так и волки. У них есть общее поведение (на- пример, привычка спать в укрытии). Li on MakeNoise Eat Субклассы наследуют все четыре метода от Animal, но пока из них переопределя- ются только MakeNoise и Eat. Вот почему на диаграмме класса пред- ставлены только эти два метода. Гиппопотамы на самом деле явля- ются речными млекопитающими! Почему бы не добавить в класс Hippo метод Swim (плавать)?
324 глава 6 расширениебазовых классов Fel ine Roam Canine Eat Sleep Animal Picture Food Hunger B oundar ies Loc atio n MakeNoise Eat S leep Roam L ion MakeNoise Eat Hippo MakeNoise Eat Swim Tiger MakeNoise Eat Dog MakeNoise Bobcat MakeNoise Eat Wo lf MakeNoise Волки и собаки едят одинако- во, поэтому мы переместили их об щий ме тод Eat в класс Canine. Три разновидности кошачьих бродят одинаково, поэтому они совместно ис- пользуют унаследо- ванный метод Roam. При этом каждый вид по-разному ест и издает разные звуки, так что все они переопределя- ют методы Eat и MakeNoise, наследу- емые от Animal. Так как Feline пере- определяет Roam, все классы, наследующие от Feline, также получают обновленную реализацию Roam (вместо реализа- ции из A nimal). Завершите свою иерархию классов. Теперь вы знаете, как упорядочить классы животных, и мы можем добавить классыFelineиCanine. Когда вы создаете свои классытак, что на вершинеиерархии находитсябазовыйкласс, а подним более конкретные субклас- сы, а у этих субклассовбудут свои субклассы,наследующиеот них,результатвашихусилийназывается иерархией классов. Идело даже невустранении дублирующегося кода,хотя безу- словно, это огромное преимуществоразумнойиерархии.Одно из преимуществ—код становитсянамного болеепростым для пониманияисопровождения. Когда выпросматриваете кодси- мулятора зоопаркаи видитеметодили свойство,определенные в классеFeline,вы сразу же видите, что эта функциональность являетсяобщейдлявсехкошачьих.Ваша иерархиястановится картой, по которой можно ориентироваться в логике про- граммы. 5 Об ъе кты Wolf и Dog облада- ют похожим поведением сна иеды,ноиз- дают разные звуки.
дальше 4 325 н аслед ов ание Dog spot = new Dog(); spot.MakeNoise(); spot.Roam(); spot.Eat(); spot.Sleep(); spot.Fetch(); Ваши возможности не ограничиваются методами, наследуемыми субклассом отбазового класса... но вы это ужезнаете!Вконцеконцов,вы уже создавалисоб- ственныеклассы. Приизменениикласса,врезультате которогоон наследует компоненты(вскоревыувидите, какэто делается в кодеC#!),вы беретеуже построен- ныйкласс ирасширяете его,добавляявнего все поля, свойства иметодыиз базового класса. Такимобразом, есливы захотите добавить методFetchвклассDog, это вполне нормально. Метод будет существовать только в классе Fog, а в классы Wolf,Canine, Animal,Hippo или любые другие классы он не попадет. Создает новый экземпляр dog вызывает верСию из клаССа dog вызывает верСию из клаССа AnimAl вызывает верСию из клаССа CAnine вызывает верСию из клаССа CAnine вызывает верСию из клаССа dog C# всегда вызывает самый конкретный метод ЕсливывызоветеметодRoam дляобъектаDog, товызван может быть только один метод — реализация из класса Animal. А если вызвать MakeNoise?Какаяизреализацийбудетвызвана? Насамом деле это определяется достаточнолегко. Методкласса Dog определяет,каксобакииздаютзвуки.Если методнаходитсявклассе Canine, он определяет поведение, общее для всех представителей семейства собачьих. Если метод находится в Animal, он описывает поведение настолькообщее,чтооноприменимо клюбомуживотному. Итак, если вы берете объектDog и вызываете для него MakeNoise, сначала C# ищет в классе Dog поведение, относящееся непосред- ственнок собакам. ЕсливDogнет метода MakeNoise, то проверяется методCanine, а после него будетпроверенметод Animal. Animal Picture F ood H unger B oun dari es Lo cati on MakeNoise Eat Sleep R oam Canine Eat Sleep Dog MakeNoise Fetch Каждый субкласс расширяет свой базовый класс и-е-рар-хи-я, сущ. структура или система класси- фикации, в которой группы или отдельные предметы ран- жируются по вертикали отно- сительно друг друга.
326 глава 6 если нужна птица, можно использовать дятла Bird Wa lk LayEggs Fly В любом месте, где может использоваться базовый класс, вместо него можно использовать один из субклассов Одна из самых полезных возможностей наследования — расширение класса. Если вашметодполучает объект Bird (Птица),товместо негоможно передать экземпляр Woodpecker (Дятел).Методуизвестнотолько то,чтоэто птица.Оннезнаетконкрет- нойразновидностиптицы, ипоэтомуможетвыполнятьтолько операции, общие для всехптиц: ходить, откладывать яйца ит.д., но не долбить деревоклювом,потомучто этоповедениеприсуще толькодятлам, — методне знает, что экземплярпредставляет именно дятла,а знаетлишь то,что этоболееобщий классBird.Для него доступны только поля, свойства и методы, являющиеся частью известного ему класса. Посмотрим, какэто работает вкоде.Передвамиметод, получающийссылкунаBird: public void IncubateEggs(Bird bird) { bird.Walk(incubatorEntrance); Egg[] eggs = bird.LayEggs(); AddEggsToHeatingArea(eggs); bird.Walk(incubatorExit); } Например,методу IncubateEggs можно передать ссылку наWoodpecker,потому что дятел является частнымслучаемптицы,именнопоэтомуклассWoodpackerнаследует отBird: public void GetWoodpeckerEggs() { Woodpecker woody = new Woodpecker(); IncubateEggs(woody); woody.HitWoodWithBeak(); } woody можно присвоить переменной Bird, потому что дятел является частным случаем птицы... ... но birdReference невозможно присвоить об- ратно переменной Woodpecker, потому что не каждая птица является дятлом! Этим объяс- няется ошибка в этой строке. Wo od pecker BeakLength HitWoodWithBeak Даже если передать IncubateEggs объект Woodpecker, ссылка все равно интерпретируется как ссылка на Bird, поэтому исполь- зоваться могут только компо- ненты класса Bird. Это должно быть понятно на интуитивном уровне. Если кто- то потребует у вас птицу и вы предложите ему дятла, то никаких претензий быть не должно. Но если у вас требуют дятла, а вы выдае- те голубя, вас не поймут. Суперкласс можетзаменятьсясубклассом,но ненаоборот— субкласс не можетзаменятьсясупер- классом. Woodpeckerможнопередать методу,получающемуссылкунаBird,ноненаоборот: public void GetWoodpeckerEggs_Take_Two() { Woodpecker woody = new Woodpecker(); woody.HitWoodWithBeak(); // Эта строка копирует ссылку Woodpecker в переменную Bird Bird birdReference = woody; IncubateEggs(birdReference); // В СЛЕДУЮЩЕЙ СТРОКЕ ПРОИСХОДИТ ОШИБКА КОМПИЛЯЦИИ!!! Woodpecker secondWoodyReference = birdReference; secondWoodyReference.HitWoodWithBeak(); }
дальше 4 327 н аслед ов ание Приведенныйнижекод взят из программы, использующеймодельклассов склас- самиAnimal, Hippo, Canine, Wolf иDog. Зачеркните все команды, которыене будут компилироваться, икраткоопишите суть проблемыдлякаждогослучая. Canine cani s = new Dog(); Wol f charon = n ew Canine(); c haron.IsArbo real = false; Hippo bailey = ne w Hippo(); bailey.R oam(); bailey.S leep(); bailey.S wim(); bailey.E at(); Dog fido = canis; A nimal visit orPet = fido; A nimal harve y = bailey; harvey.R oam(); harvey.S wim(); harvey.S leep(); harvey.E at(); Hippo brutus = ha rvey; brutus.R oam(); brutus.S leep(); brutus.S wim(); brutus.E at(); Canine lond on = new Wol f(); Wol f egypt = lo ndon; egypt.HuntWithPack(); egypt.HuntWithPack(); egypt.Alpha InPack = fal se; Dog rex = london; rex.Fetch(); Animal Picture F ood Hu nger B ounda ri es Loc ati on MakeNoise Eat Sleep Ro am Hippo MakeNoise Eat Sw im Dog MakeNoise Fet ch Breed Wolf MakeNoise HuntWithPack Canine Eat Sleep AlphaInPack IsArboreal Возьми в руку карандаш
328 глава 6 ответ к упражнению Шесть из следующих команд не будут компилироваться, потому что они конфликтуют смоделью классов.Выможетеубедиться в этом самостоятель- но! Постройте свою версию модели классов с пустыми методами, введите код ипрочитайте сообщения об ошибках компилятора. Canine canis = new Dog(); Wolf charon = new Canine(); charon.IsArboreal = false; Hippo bailey = new Hippo(); bailey.Roam(); bailey.Sleep(); bailey.Swim(); bailey.Eat(); Dog fido = canis; Animal visitorPet = fido; Animal harvey = bailey; harvey.Roam(); harvey.Swim(); harvey.Sleep(); harvey.Eat(); Hippo brutus = harvey; brutus.Roam(); brutus.Sleep(); brutus.Swim(); brutus.Eat(); Canine london = new Wolf(); Wolf egypt = london; egypt.HuntWithPack(); egypt.HuntWithPack(); egypt.AlphaInPack = false; Dog rex = london; rex.Fetch(); Animal Picture F ood H unger B oun dari es Lo cati on MakeNoise Eat Sleep R oam Hippo MakeNoise Eat Swim Dog MakeNoise Fetch Bre ed Wo lf MakeNoise HuntWithPack Canine Eat Sleep AlphaInPack IsArboreal Wolf является субклас- сом Canine, так что объект Canine нельзя присвоить перемен- ной Wolf. Взгляните на это так: волк являет- ся частным случаем семейства собачьих, но не каждый представи- тель этого семейства является волком. Хотя переменная canis является ссылкой на объект Dog, переменная относится к типу Canine, так что присвоить ее Dog нельзя. harvey — ссылка на объект Hippo, но переменная harvey относится к типу Animal, так что она не может исполь - зоваться для вызова метода Hippo.Swim. Не работает по той же при- чине, по которой не работа- ет Dog fido = canis;. harvey может указывать на объект Hippo, но переменная отно- сится к типу Animal, а пере- менную Animal нельзя при- своить переменной Hippo. Та же проблема! Wolf можно при- своить Canine, но Canine нельзя присвоить Wolf... ...и разумеется, Wolf нельзя присвоить Dog. Возьми в руку карандаш Решение
дальше 4 329 н аслед ов ание ¢ Команда switch позволяет сравнить одну пере- менную с несколькими возможными вариантами. Код блока выполняется при совпадении значения. Если ни один вариант не подходит, выполняется блок default. ¢ Наследование используется для построения классов, связанных друг сдругоми обладающих общим поведе- нием. Надиаграммах классов наследование обознача- ется линиями со стрелками. ¢ Если имеются два класса, которые являются конкрет- ными частными случаями чего-то более общего, их можно определить как наследующие от одногобазово- го класса. В таком случае каждый из классов становит- ся субклассом общего базового класса. ¢ Набор классов, представляющихразличные сущности, называется моделью классов. Модель может вклю- чать классы, образующие иерархию субклассов и ба- зового класса. ¢ Термины «родитель», «суперкласс» и «базовый класс» часто используются как синонимы. Кроме того, термины «расширять» и «наследовать от» означают одно и то же. ¢ Термины «дочернийкласс» и «субкласс» также явля- ются синонимами. Мы говорим, что субклассрасширя- ет свой базовый класс. ¢ Когда субкласс изменяет поведение одного из унасле- дованных методов, мы говорим, что он переопреде- ляет метод. ¢ C# всегда вызывает наиболее конкретную версию метода. Если методбазовогокласса использует метод или свойство, переопределенные в субклассе, то бу- дет вызвана переопределенная версия из субкласса. ¢ Всегда используйте ссылку на субкласс вместо ба- зового класса. Если метод получает параметр Animal, а Dog расширяет Animal, методуможно передать аргу- мент Dog. ¢ Субкласс всегда может использоваться вместо ба- зового класса, от которого он наследует; но базовый класс далеко не всегда может использоваться вместо расширяющего его субкласса. КЛЮЧЕВЫЕ МОМЕНТЫ Все это здорово, конечно... теоретически. Но как это поможет в моем приложении- калькуляторе? Оуэн задал очень хороший вопрос. Вернитесь к приложению, которое вы построили для Оуэна, — к аль кулятору повреждений от меча и стре- лы. Как бы вы использовали наследование и субклассы для улучшения к од а? ( Вн иман ие, сп ойлер: мы займем ся э тим п оздн ее, читайте даль ше. Мозговой штурм
330 глава 6 субкласс наследует компоненты отбазового класса Расширение базового класса Чтобы объявить,что определяемый класс наследует отбазового класса, используйтедвоеточие (:).Класс становится субклассоми получаетвсе поля, свойства иметодыот класса, от которогоон наследует.КлассBird является субклассом Vertebrate: Bird Wingspan Fly Vert eb rat e NumberOfLegs Eat clas s Vertebrate { public int Legs { get; set; } public void Eat() { // код питани я } } clas s Bird : Vertebrate { public double Wingspa n; public void Fly() { // code to make a bird fly } } public voi d Main( string[] args) { Bird t weety = new Bi rd(); Consol e.Write Line(tw eety.W ingspan); tweety .Fly(); tweety .Legs = 2; Consol e.Write(tweety .Eat()); } Субкласс, расши- ряющий базовый класс, наследует все его компоненты: все поля, свойства и методы, содер- жащиеся в базовом кла ссе. Все о ни автоматически добавляются в суб- кла сс. Двоеточие в классе Bird означает, что класс наследует от классаVertebrate. Эт о озн ачает, что о н насл едует все поля, свойства и методы от Vertebrate. Базовый классуказывается за двоеточием в объявлении класса. В данном случаеBird расширяет Vertebrate. Так как класс Bird расширяет Vertebrate, каждый экземпляр Bird также содержит компоненты, опре- деленные в клас- се Vert eb rat e. tweety является экземпляром Bird и поэтому содержит методы, свойства и поля Bird.
дальше 4 331 н аслед ов ание Bird Wal k Fly La yEgg s Мы знаем, что наследование добавляет в субкласс поля, свойства и методы базового класса... Вы уже сталкивались с наследованием, когда субкласс должен унаследовать все методы,свойства иполябазового класса. .. . но н екоторые птицы не летают! Что делать, если базовый класс содержит метод, который должен быть изменен вашим субклассом? class Bird { public void Fly() { /* Код реализации полетов } public void LayEggs() { ... }; public void PreenFeathers() { ... }; } class Pigeon : Bird { public void Coo() { ... } } public void SimulatePigeon() { Pigeon Harriet = new Pigeon(); // Так как Pigeon является субклассом // Bird, мы можем вызывать методы обоих классов. Harriet.Walk(); Harriet.LayEggs(); Harriet.Coo(); Harriet.Fly(); } class Penguin : Bird { public void Swim() { ... } } public void SimulatePenguin() { Penguin Izzy = new Penguin(); Izzy.Walk(); Izzy.LayEggs(); Izzy.Swim(); Izzy.Fly(); } Bird Wal k Fly LayEggs Pigeon Coo Pigeon Coo Peng ui n Sw im Если бы эти классы входили в ваш симулятор зоо- парка, к ак бы вы решали проблему летающих пинг- винов? Мозговой штурм Кажется, у нас проблема. Пингвины — птицы, а класс Bird содержит метод Fly, но пингвины летать не должны. Было бы замечательно, если бы при вызове Fly для пингвина вывод илос ь пр ед упре жде ние . Этот код компилируется, потому что Penguin расширя- ет Bird. Можно ли изменить класс Penguin так, чтобы при вызове Fly для пингвина выво- дилось предупреждение?
332 глава 6 virtual и override Субкласс может переопределять методы для изменения или замены унаследованных компонентов Иногдасубклассдолжен унаследоватьбольшую частьповедения базовогокласса, но невсе. Чтобы изменить поведение, унаследо- ванное классом, можно переопределить методыилисвойства, заменивихновымиметодамиили свойствамис темиже именами. Припереопределении метода новый метод должен обладать точно такой же сигнатурой, как у переопределяемого метода базового класса. Например, в случае с пингвинами это озна- чает, что он называется Fly,возвращает void и вызываетсябез параметров. Добавьте ключевое слово virtual в метод базового класса. Субклассможетпереопределить только метод, помеченный ключевым словом virtual. Вклю- чениеvirtual вобъявлениеметода Fly сообщаетC#,что субклассу классаBirdразрешенопере- определить метод Fly. class Bird { public virtual void Fly() { // code to make the bird fly } } Добавьте ключевое слово override в одноименный метод субкласса. Метод субкласса долженобладать точно такой жесигнатурой — той же комбинациейтипа воз- вращаемого значения и параметров, ив объявлении должно использоваться ключевое слово override. Теперь объектPenguin выводитпредупреждение припопытке вызова метода Fly. class Penguin : Bird { public override void Fly() { Console.Error.WriteLine("WARNING "); Console.Error.WriteLine("Flying Penguin Alert"); } } 1 2 Добавление ключевого слова virtual в метод Fly сообщает C#, что субклассу разрешено переопределить метод. пе-ре-о-пре-де-лить, гл. Определить заново или по-другому; дать новое определение. Чтобы переопределить ме- тод Fly, добавьте идентичный метод в субкласс и используй- те ключевое слово override. Мы использовали Console.Error для вывода сообщений об ошибках в стан- дартный поток ошибок (stderr), который обычно используется консольными при- ложениями для вывода описаний ошибок и важной диагностической информации. Маши крыльями, Боб. Уверен, мы скоро полетим!
дальше 4 333 н аслед ов ание Ниже приведена короткая программа C#. В про- грамме отсутствует один блок! Ваша задача — со- поставить один из вариантов кода (в левом столб- це) с выводом, который вы получите при вставке этого блока. Не все строки вывода будут исполь- зованы, и некоторые из строк могут использо- ваться более одного раза. Соедините линиями блокикода с соответствующим им выводом. Сюда вставля- ется блок кода (три строки). class A { public int ivar = 7; public ___________ string m1() { return "A's m1, "; } public string m2() { return "A's m2, "; } public ___________ string m3() { return "A's m3, "; } } classB:A{ public ___________ string m1() { return "B's m1, "; } } classC:B{ public ___________ string m3() { return "C's m3, " + (ivar + 6); } } class Mixed5 { public static void Main(string[] args) { Aa=newA(); Bb=newB(); Cc=newC(); Aa2=newC(); string q = ""; Console.WriteLine(q); } } Блоки кода: Вывод: q += b.m1(); q += c.m2(); q += a.m3(); q += c.m1(); q += c.m2(); q += c.m3(); q += a.m1(); q += b.m2(); q += c.m3(); q += a2.m1(); q += a2.m2(); q += a2.m3(); } } } } a=6; 56 b=5; 11 a=5; 65 Сопос тав ь те! Точка входа в программу Инс т ру кции: 1. Заполните четыре пропуска в коде. 2. Сопоставьте предложенные варианты кода с выводом. (Не упрощайте свою задачу, вводя код в IDE, вы узнаете намного больше, если определите результат на бумаге!) Подсказка: очень хо- рошо подумайте над тем, что означает эта строка. A's m1, A's m2, C's m3, 6 B's m1, A's m2, A's m3, A's m1, B's m2, C's m3, 6 B's m1, A's m2, C's m3, 13 B's m1, C's m2, A's m3, A's m1, B's m2, A's m3, B's m1, A's m2, C's m3, 6 A's m1, A's m2, C's m3, 13 Проведи- те линию от каждо- го блока из трех строк кода к строке вы- в од а, кото- рая будет получена при разме- щении блока в прямо- угольнике. Упражнение
334 глава 6 трен ир уемся в р асшир ении к л ассов q += b.m1(); q += c.m2(); q += a.m3(); q += c.m1(); q += c.m2(); q += c.m3(); q += a.m1(); q += b.m2(); q += c.m3(); q += a2.m1(); q += a2.m2(); q += a2.m3(); A's m1, A's m2, C's m3, 6 B's m1, A's m2, A's m3, A's m1, B's m2, C's m3, 6 B's m1, A's m2, C's m3, 13 B's m1, C's m2, A's m3, A's m1, B's m2, A's m3, B's m1, A's m2, C's m3, 6 A's m1, A's m2, C's m3, 13 } } } } virtual class A { public ___________ string m1() { ... public ___________ string m3() { } vir tu al classB:A{ public ___________ string m1() { ... classC:B{ public ___________ string m3() { override override a=6; 56 b=5; 11 a=5; 65 Ссылканасубкласс всегда может использо- ваться вместо ссылки на базовый класс, по- тому что конкретное может использоваться вместо общего. Таким образом, следующая строка: Aa2=newC(); означает,чтовысоздаете новыйобъектC,затем создаете переменнуюa2, объявленную как ссыл- ка на A, и присваиваете переменной ссылку на объект.Такие именахороширазвечтов голово- ломках—они слишкомнепонятны. Следующие строки следуют той же схеме, но используют болееочевидные имена: Canine fido = new Dog(); Bird pidge = new Pigen(); Feline rex = new Lion(); В: Команда switchделает то же, что и серия командif/else, верно? Получается, что она избыточна? О:Вовсе нет. Во многих ситуациях команды switch читаются лучше, чем команды if/else. Допустим, вы отображаете меню в консольном приложении и пользователь может нажать клавишу, чтобы выбрать один из десяти разных вариантов. Как будут смо- треться 10 команд if/else подряд? Мы считаем, что команда switchбудетболееэлегантной иудобочитаемой.Вы сразу видите, что с чем сравнивается, где обрабатывается каждый вариант и что происходит поумолчанию, если пользователь выбрал отсутствую- щий вариант. Крометого, в if/else на удивлениелегкослучайно пропустить else. Если пропущенный блок else находится где-то всерединедлиннойцепочкикомандif/else,возникает коварная ошибка,которую наудивлениетруднообнаружить.В одних случаях лучшечитаетсякомандаswitch,в другихлучше читаютсякоманды if/else. Вы сами пишете код в том виде, который (по вашему мнению) будетнаиболее простым и наглядным. В: Почему стрелка направлена вверх, от субкласса к базо- вому классу?Разведиаграмма небудетболее логичной, если стрелка направлена сверху вниз? О: На первый взгляд это кажется более логичным, но не так точно. Когда вы создаете класс, наследующий от другого класса, эти отношения встраиваютсяв субкласс — базовый класс остается неизменным. Его поведение нисколько не изменяется, когда вы добавляете класс, наследующий от него. Базовый класс даже не знает осуществовании нового класса.Его методы,поляи свойства остаются неизменными, но субкласс изменяет его поведение. Каж- дый экземпляр субкласса автоматически получает все свойства, поля и методы от базового класса — и все это делается простым добавлениемдвоеточия!Вот почему стрелка надиаграмме идет от субкласса к базовому классу, от которого он наследует. Упражнение Реше ни е Задаваемые вопросы часто Сопос та вь те!
дальше 4 335 н аслед ов ание Немного потренируемся в расширении базовых классов. Ниже приведен метод Main для про- граммы, в которой моделируются птицы, несущие яйца. Ваша задача — реализовать два суб- класса класса Bird. 1. Метод Main запрашивает у пользователя тип птицы и количество яиц: static void Main(string[] args) { while (true) { Bird bird; Console.Write("\nPress P for pigeon, O for ostrich: "); char key = Char.ToUpper(Console.ReadKey().KeyChar); if (key == 'P') bird = new Pigeon(); else if (key == 'O') bird = new Ostrich(); else return; Console.Write("\nHow many eggs should it lay? "); if (!int.TryParse(Console.ReadLine(), out int numberOfEggs)) return; Egg[] eggs = bird.LayEggs(numberOfEggs); foreach (Egg egg in eggs) { Console.WriteLine(egg.Description); } } } 2. Добавьте класс Egg — конструктор задает размер ицвет яйца. class Egg { public double Size { get; private set; } public string Color { get; private set; } public Egg(double size, string color) { Size = size; Color = color; } public string Description { get { return $"A {Size:0.0}cm {Color} egg"; } } } 3. КлассBird, который вы будете расширять: class Bird { public static Random Randomizer = new Random(); public virtual Egg[] LayEggs(int numberOfEggs) { Console.Error.WriteLine("Bird.LayEggs should never get called"); return new Egg[0]; } } 4. Создайте класс Pigeon, расширяющий Bird. Переопределите метод LayEggs и настройте класс так, чтобы он нес яйца белого цвета ("white") размером от 1 до 3 сантиметров. 5. Создайте класс Ostrich, также расширяющий Bird. Переопределите метод LayEggs и настройте класс так, что - бы он нес яйца в крапинку ("speckled") размером от 12 до 13 сантиметров. Вывод программыдолжен выглядеть так: Press P for pigeon, O for ostrich: P How many eggs should it lay? 4 A 3.0cm white egg A 1.1cm white egg A 2.4cm white egg A 1.9cm white egg Press P for pigeon, O for ostrich: O How many eggs should it lay? 3 A 12.1cm speckled egg A 13.0cm speckled egg A 12.8cm speckled egg Упражнение
336 глава 6 динамика и механика Ниже приведены классы Pigeon и Ostrich. Каждый из них содержит собственную версию мето- да LayEggs, которая использует ключевое слово override в объявлении метода. Ключевое слово override означает, что реализация субкласса заменяет реализацию, унаследованную от базового класса. Pigeon является субклассом Bird, поэтому если вы переопределили метод LayEggs, а потом создали объект Pigeon и присвоили его переменной Bird с именем bird, при вызове bird.LayEggs будет вызван метод LayEggs, определенный в Pigeon. class Pigeon : Bird { public override Egg[] LayEggs(int numberOfEggs) { Egg[] eggs = new Egg[numberOfEggs]; for (int i = 0; i < numberOfEggs; i++) { eggs[i] = new Egg(Bird.Randomizer.NextDouble() * 2 + 1, "white"); } return eggs; } } СубклассOstrich работает так же, как Pigeon. В обоих классах ключевое слово override в объявлении метода LayEggs означает, что новый метод заменяет реализацию LayEggs, унаследованную от Bird. Остается лишь создать набор яиц нужного цвета и размера. class Ostrich : Bird { public override Egg[] LayEggs(int numberOfEggs) { Egg[] eggs = new Egg[numberOfEggs]; for (int i = 0; i < numberOfEggs; i++) { eggs[i] = new Egg(Bird.Randomizer.NextDouble() + 12, "speckled"); } return eggs; } } Упражнение Решение
дальше 4 337 н аслед ов ание Некоторые компоненты реализованы только в субклассе Вовсемкоде, который вы видели до настоящего момента, обращения к компонентам производились из- вне, как, например, в методе Main в только чтонаписанном коде вызывалсяметодLayEggs. Наследование по-настоящемураскрывает свойпотенциал,когдабазовый классиспользуетметодилисвойство,реализо- ванныев субклассе. Рассмотрим пример.Внашемсимуляторе зоопаркаустановленыторговые автоматы, вкоторыхпосетители могутпокупатьгазировку, леденцыи кормдляживотныхв контактной зоне зоопарка. class V ending Machine { public virtual string Item { get; } protected virtual bool CheckAm ount(decimal money) { return false; } public string Dispense(decimal money) { if (Ch eckAmou nt(money)) return It em; else r eturn " Please enter the right amo unt"; } } VendingMachine — базовый класс для всех торговых автоматов. Он содержит кодпродажи товаров, но сами товары не определяются. Методдля проверки того,внес липокупатель правильную сумму, всегда возвращаетfalse. Почему?Потому,что онбудетреализован в субклассе. Субкласс для продажикорма для животных в контактной зоне выглядит так: class A nimalF eedVend ingMach ine : Vending Machine { public ove rride string Item { get { return "a handful of animal feed"; } } protected overrid e bool CheckA mount(decimal money) { return money >= 1.2 5M; } } Классиспользует ключевое слово protected. С этим моди- фикатором доступа компонент класса становится откры- тым только для его подклассов, но остается приватным для всех остальныхклассов. Для инкапсуляции используется ключевое слово protected. Метод CheckAmount объявляется с клю- чевым словом protected, потому что он ни при каких условияхнедолжен вызываться другим классом, так что обращение к нему возможно только из класса VendingMachine и его субклассов. Ключевое слово override со свойством рабо- тает точно так же, как при переопределении метода.
338 глава 6 с# вызывает самую конкретную реализацию метода Анализ переопределения в отладчике Отладчик поможет понять, что же именно происходит, когда мы создаем экземплярAnimalFeedVen- dingMachine иобращаемсяк нему с запросом на продажукорма. Создайте новыйпроект консольного приложения, после чего выполните следующие действия: Добавьте метод Main. Метод должен содержать следующий код: class Program { static void Main(string[] args) { VendingMachine vendingMachine = new AnimalFeedVendingMachine(); Console.WriteLine(vendingMachine.Dispense(2.00M)); } } Добавьте классы VendingMachine и AnimalFeedVendingMachine. Когда они будут добавлены, попробуйте включить следующую строку кода вметодMain: vendingMachine.CheckAmount(1F); Выполучитесообщение об ошибке компиляции из-за ключевого слова protected,потому что к методам с таким модификатором может обращаться только сам класс VendingMachine или его субклассы: Удалитедобавленную строку,чтобы приложениестроилось нормально. Установите точку прерывания в первой строке метода Main. Запустите программу. Когда она достигнет точки прерывания, воспользуйтесь командой Step Into (F10) для пошагового выполнения кода. Вотчто приэтомпроисходит: Ì Программа создает экземпляр AnimalFeedVendingMachine и вызывает его метод Dispense. Ì Методопределяетсятольков базовом классе,поэтому вызываетсяреализацияVendingMachine. Dispense. Ì В первой строке VendingMachine.Dispense вызывается защищенный (protected) метод CheckAmount. Ì CheckAmount переопределяется в субклассе AnimalFeedVendingMachine, из -за чего VendingMachine.Dispense вызывает метод CheckAmount, определенный в AnimalFeedVen- dingMachine. Ì Эта версияCheckAmount возвращает true, поэтому Dispense возвращает свойство Item. AnimalFeedVendingMachine также переопределяет это свойство. 1 2 3 Принц ипы о тла дки Мы уже использовали отладчик Visual Studio для поиска ошибок в коде. Но этот ин- струмент также прекрасно подходит для изучения C# и анализа — как в этой врезке «Принципы отладки» (см. выше), где мы изучали работу переопределения. А вы сможе- те предложить другие способы экспериментирования с переопределением?
дальше 4 339 н аслед ов ание Если субкласс переопреде- ляет метод в своем базовом классе, то всегда будет вы- зываться более конкретная реализация, определенная в субклассе, даже при вы- зове из метода базового кла сса . Для использования virtual и override есть веские причины! Ключевыесловvirtual и override существуют нетолько длякрасоты. Они реальновлияютнаработу вашейпрограммы. Ключевое слово virtual со- общаетC#, чтокомпонент (метод, свойствоили поле)можетрасширяться— безнегопереопределениевообще невозможно. Ключевоеслово override сообщает C#,чтовы расширяете компонент.Если опустить ключевоеслово override всубклассе,вы создадитесовершеннонесвязанный метод, который просто по случайности имеет такое же имя. Звучит немного странно, нетакли?Но на самом делевсевполнеразумно— чтобыпонять, какработаютключевые слова virtual и override,лучше всего начать с написаниякода. Построимреальныйпример дляэкспериментов. Что-то я не пойму, для чего нужны эти ключевые слова «virtual» и «override». Без них IDE выдает предупреждение, но это ничего не значит... программа все равно работает! То есть ключевые слова поставить можно, если это вроде как «положено», но по-моему, мы сами выдумываем себе трудности на ровном месте.
340 глава 6 изучение virtual и override Создайте новое консольное приложение и добавьте класс Safe. Кодкласса Safe: class Safe { private string contents = "precious jewels"; private string safeCombination = "12345"; public string Open(string combination) { if (combination == safeCombination) return contents; return ""; } public void PickLock(Locksmith lockpicker) { lockpicker.Combination = safeCombination; } } Добавьте класс, представляющий владельца сейфа. Владелец сейфарассеян и часто забывает свой отлично защищенный пароль от сейфа. Добавьте класс SafeOwner для представления владельца: class SafeOwner { private string valuables = ""; public void ReceiveContents(string safeContents) { valuables = safeContents; Console.WriteLine($"Thank you for returning my {valuables}!"); } } 1 2 Объект Safe хранит ценности в поле contents. Чтобы он вер- нул их, метод Open должен быть вызван с правильной комбинацией, или... слесарь откроет замок. Мы добавим класс Locksmith, кото- рый может открыть кодовый замок и получить нужную комбинацию вы- зо вом мето да PickL ock с п еред ачей ссылки на себя. Чтобы предоставить Locksmith комбинацию, объект Safe использует свойство Combination, доступное только для записи. Построение приложения для изучения virtual и override Исключительноважнойчастью наследованиявC#является расширение компонентовклассов. Таким образомсубклассможетунаследовать часть своего поведенияотбазового класса, переопределяяотдельные компонен- ты, и именно здесь вигрувступают ключевые слова virtual иoverride. Ключевоеслово virtual определяет компоненты класса, которые могут расширяться. Если выхотитерасширить тот или инойкомпонент, ондол- женбыть объявлен с ключевымсловомoverride.Создадимнаборклассов дляэкспериментов сvirtualи override. Первый класс представляетсейф с бриллиантами, а потом мы построим класс, представляющийхитрых взломщиков, которые пытаются этибриллиантыукрасть. Сделайте это!
дальше 4 341 н аслед ов ание Добавьте класс Locksmith, представляющий слесаря. Есливладелец сейфавоспользуетсяуслугамипрофессионального слесарядляоткрытиясейфа, он ожидает,чтовсесодержимоесейфаостанетсявнеприкосновенности. Именно это делает метод Locksmith.OpenSafe: class Locksmith { public void OpenSafe(Safe safe, SafeOwner owner) { safe.PickLock(this); string safeContents = safe.Open(Combination); ReturnContents(safeContents, owner); } public string Combination { private get; set; } protected void ReturnContents(string safeContents, SafeOwner owner) { owner.ReceiveContents(safeContents); } } Добавьте класс JewelThief, представляющий вора, который хочет украсть бриллианты. Вобщей схеме появляется вор — и что самое неприятное, он также является исключительно опытным слесарем иумеет открыватьсейфы.Добавьте классJewelThief,расширяющийLocksmith: class JewelThief : Locksmith { private string stolenJewels; protected void ReturnContents(string safeContents, SafeOwner owner) { stolenJewels = safeContents; Console.WriteLine($"I'm stealing the jewels! I stole: {stolenJewels}"); } } Добавьте метод Main, в котором экземпляр JewelThief крадет бриллианты. Наступает момент длякражи века!В методеMainэкземплярJewelThiefпроникает вдом иисполь- зуетунаследованныйметодLocksmith.OpenSafe дляполучения комбинации. Каквыдумаете, что произойдет при его выполнении? static void Main(string[] args) { SafeOwner owner = new SafeOwner(); Safe safe = new Safe(); JewelThief jewelThief = new JewelThief(); jewelThief.OpenSafe(safe, owner); Console.ReadKey(true); } 3 4 5 JewelThief расширяет Locksmith, наследуя метод OpenSafe и свойство Combination, но его метод ReturnContents крадет бриллианты, вме- сто того чтобы вернуть их владельцу. КАКОЕ КОВАРСТВО! Метод OpenSafe класса Locksmith узнает комби- нацию, открывает сейф и вызывает ReturnContents для безопасной передачи ценностей владельцу. Прочитайте код вашейпрограммы.Прежде чем запускать его, напишите, что, по вашему мне- нию, он выведет на консоль. (Подсказка: про- анализируйте, что именно класс JewelThief на- следует от Locksmith.) MI N I Возьми вруку карандаш
342 глава 6 сокрытие и переопределение Субкласс может скрывать методы базового класса Запустите программу JewelThief. Она выведет следующее сообщение: Thank you for returning my precious jewels! Выожидалидругого?Возможно, чего-то такого: I'm stealing the jewels! I stole: precious jewels Похоже, объектJewelThief сработал как обычный объектLocksmith!Что же произошло? Сокрытие методов и переопределение методов Почему жеобъектJewelThief при вызовеего метода ReturnContents действовал как объектLocksmith? Это связано с тем, как в классе JewelThief объявляется метод ReturnContents. В предупреждающем со- общении,которое выводится прикомпиляции программы, содержится яснаяподсказка: Так как класс JewelThief наследует от Locksmith и замещает метод ReturnContents собственным методом, может показаться, что JewelThief переопределяет метод ReturnContents класса Locksmith, но на самом делепроисходит нечто иное. Веро- ятно, вы ожидали, что JewelThief переопределит метод (сейчас мы обсудим эту воз- можность),но вместо этого JewelThief скрывает его. Этидвеситуации очень сильно отличаются друготдруга. Когда субкласс скрываетметод,онзамещает (с технической точки зрения — переобъявляет) одноименный метод базового класса. Таким образом, сейчас субкласс содержит два разных метода с одинаковыми именами: один наследуется отбазового класса,а другойопределяется вэтом классе. Используйте ключевое слово new для сокрытия методов Присмотритесь к предупреждающему сообщению. Конечно, мы знаем, что предупреждения нужно читать, но иногда мы этого не делаем... верно?Так чтотеперь прочитайте, чтоже внемсказано: «Используйтеключевоеслово new, если предполагалось сокрытие». Вернитесь кпрограмме идобавьтеключевоеслово new: new public void ReturnContents(Jewels safeContents, Owner owner) Кактолько вы добавите new в объявление метода ReturnContents класса JewelThief, предупреждение исчезнет— новаш кодвсе равно небудет работать так,как ожидалось! Внемпо-прежнемувызываетсяметодReturnContents, определенныйвклассе Locksmith.Почему?Потому, что методReturnContents вызываетсяиз метода, определенного вклассеLocksmith, а конкретноизLocksmith.OpenSafe, при том что вызов был инициированобъектомJewelThief.Если классJewelThief толькоскрывает методReturnContentsкласса Locksmith,тоегособственный метод ReturnContents никогда не будет вызван. Если субкласс просто добавляет метод с таким же именем, как у метода базового класса, он только скрывает метод базового класса вместо того, что- бы переопреде- лять ег о. Вроде бы C# должен вызы- вать самый конкретный метод, не так ли? Тогда почему мы не назвали его JewelThief.ReturnContents? Jewel Th ief Locksmith.ReturnContents JewelThief.ReturnContents M I N I Возьми в руку карандаш Решение
дальше 4 343 н аслед ов ание Использование разных ссылок для вызова скрытых методов Теперь мы знаем,что JewelThief только скрывает свой метод ReturnContents (вместо того, чтобы пере- определять его). В результате он ведет себя как объектLocksmith каждыйраз, когда он вызывается как объект Locksmith. JewelThief наследует одну версию ReturnContents от Locksmith и определяет вторую версию этого метода; отсюда следует, что вклассе существуютдва одноименных метода и онидолжны вызываться двумяразнымиспособами. Существуют два разных способавызова метода ReturnContents. Еслиувас имеетсяэкземплярJewelThief, вы можете воспользоватьсяпеременной-ссылкойнаJewelThiefдля вызова новогометода ReturnContents. Еслииспользовать для вызова переменную-ссылку на Locksmith,будет вызван метод ReturnContents. Воткакэто делается: // Субкласс JewelThief скрывает метод в базовом классе Locksmith, так что вы // можете получить разное поведение для одного объекта в зависимости от того, // какая ссылка использовалась для вызова! // Объявление объекта JewelThief как ссылки на Locksmith заставляет его вызвать // метод ReturnContents базового класса. Locksmith calledAsLocksmith = new JewelThief(); calledAsLocksmith.ReturnContents(safeContents, owner); // Если объект JewelThief объявляется в виде ссылки на JewelThief, то вызван // будет метод ReturnContents() класса JewelThief, потому что он скрывает // одноименный метод базового класса. JewelThief calledAsJewelThief = new JewelThief(); calledAsJewelThief.ReturnContents(safeContents, owner); Сможетели вы понять, как заставить JewelThief переопределить метод ReturnContents, вместо того чтобы просто скрыть его? Удастся ли вам сделать это до того, как вы перейдетек следующему разделу? В: Я так инепонял, почему методы называются «виртуаль- ными» (virtual), — мн е о ни ка жутся вполне реальными. Что в них виртуального? О:Этот термин связан с особенностями внутренней реализации таких методов в .NET. В ней используется структура данных, на- зываемая таблицей виртуальных методов (или v-таблицей). Этатаблица используется в .NET для отслеживания того, какие методы были унаследованы, а какие были переопределены. Не беспокойтесь: вам не нужно знать, как работает этот механизм, чтобы пользоваться виртуальными методами. В:Выговорили о замене суперкласса ссылкой на субкласс. Можноповторить ещеразок? О:Еслинавашейдиаграмме один классрасположен вышедругого, класс, расположенный выше, более абстрактен по сравнению с нижним классом. Более конкретные классы (такие, как Shirt или Car) наследуют от более абстрактных (таких, как Clothing или Vehicle). Если вам подойдет любое транспортное средство, то по- дойдет и машина, и грузовик, и мотоцикл. Если вам нужна машина, то мотоцикл не подойдет. Наследованиеработает точно так же. Если у вас есть метод, полу- чающий параметрVehicle, а класс Motorcycle наследуетот класса Vehicle, то методу можно передать экземпляр Motorcycle. Если метод получает параметрMotorcycle, передать произвольный объ- ект Vehicle неудастся, потому что это можетбыть экземплярVan. Ведь тогда C# не будет знать, что делать, когда метод пытается обратитьсяк свойствуHandlebars,котороеесть толькоу мотоциклов. Задаваемые вопросы часто
344 глава 6 ключевые слова действительно важны Использование ключевых слов override и virtual для наследования поведения Мы хотим, чтобы классJewelThief всегда использовал свою реализацию ReturnContents, независимо от способа вызова. Именно так в нашем представлении должно работать наследование в большинстве случаев: субкласс может переопределить метод базового класса, чтобы вместо него вызывался метод субкласса. Начнитес добавленияключевого слова overrideпри объявленииметода ReturnContents: class JewelThief { protected override void ReturnContents (string safeContents, SafeOwner owner) Но этоеще не все. Если вы ограничитесь добавлением ключевого слова override в объявление класса, компилятор выдаст сообщение обошибке: Снова присмотритесь повнимательнее к тексту и прочитайте описание ошибки. JewelThief не может переопределить унаследованный метод ReturnContents, потому что он не помечен ключевым словом virtual,abstract или override вLocksmith. Ксчастью, эта ошибка исправляется очень быстро. Пометьте метод ReturnContents класса Locksmith ключевым словом virtual: class Locksmith { protected virtual void ReturnContents (string safeContents, SafeOwner owner) Сновазапуститепрограмму.На этотраз мы получаем вывод,к которомустремились: I'm stealing the jewels! I stole: precious jewels Соедините каждое из следующих описанийс ключевым словом,которое оно описыв а ет. 1. Метод, обращение к которому возможно только изэкземпляра тогоже класса. 2. Метод,которыйможет быть замененодноименным методом всубклассе. 3. Метод, обращение к которому возможно из экземпляра любого другого класса. 4. Метод, который скрывает другой одноименный метод в суперклассе. 5. Метод,которыйзаменяетметод, определенныйвсуперклассе. 6. Метод, обращение к которому возможно только из компонента класса или его субкласса. virtu al new ov erri de protect ed privat e public 1. private 2. virtual 3. public 4. new 5. override 6. protected Возьми в руку карандаш
дальше 4 345 н аслед ов ание Если вы хотите пере- определить метод в ба- зовом классе, всегда помечайте его ключевым словом virtual и всегда используйте ключевое слово override каждый раз, когда вы хотите пере опред ели ть мет од в субклассе. Если вы не будете следить за этим, то в конечном итоге это может привести к непреднамеренному сокрытию методов. Когда я строю собственные иерархии классов, обычно я хочу переопределять методы, а не скрывать их. Но если я хочу их скрыть, то я всегда использую ключевое слово new, верно? Точно. В большинстве случаев требуется переопреде- лять методы, но вариант с сокрытием тоже возможен. Когда вы работаете с субклассом, расширяющим базовый класс, то, скорее всего,вы хотитепереопределить их, а не скрывать. Итак, когда вы получаете предупреждение ком- пилятора о сокрытии метода, обратитена него внимание! Убедитесь в том, что вы планировали именно скрыть ме- тод, а не просто забыли добавить ключевые слова virtual иoverride.Есливы всегда правильноиспользуете ключевые слова virtual, override и new,подобныепроблемыу вас ни- ко гда не воз никну т!
346 глава 6 Дажекогда вы переопределяете метод или свойство вбазовом классе,иногда бывает нужно сохранить доступ кнему.Ксчастью, ключевое слово baseпозволяет обратитьсяклюбомукомпонентубазовогокласса. Все животные едят, поэтому класс Vertebrate содержит метод Eat, получающий в параметре объект Food. class Vertebrate { public virtual void Eat(Food morsel) { Swallow(morsel); Digest(); } } Хамелеоны едят, захватывая пищу длинным языком. Соответственно, класс Chameleon наследует от Vertebrate, но переопределяет Eat. class Chameleon : Vertebrate { public override void Eat(Food morsel) { CatchWithTongue(morsel); Swallow(morsel); Digest(); } } Вместо того чтобы дублировать код, можно воспользоваться ключевым словом base для вызова переопределенного метода. Теперь нам доступна как старая, так и новая версияEat. class Chameleon : Vertebrate { public override void Eat(Food morsel) { CatchWithTongue(morsel); base.Eat(morsel); } } 1 2 3 обходной путь Субкласс может обратиться к своему базовому классу при помощи ключевого слова base C hamel eon NumberOfLegs Color TongueLe ngth Eat ChangeColor GripBranch C a tchW it hTongue Vertebrate NumberOfLegs Eat Метод Chameleon.Eat должен вы- звать CatchWithTongue, но после этого он идентичен методуEat переопределяемого базового класса Vert eb rat e. Обновленная версия метода из базового класса использует ключевое слово base для вызова метода Eat в базовом классе. Те- перь дублирующийся код отсут- ствует, так что если когда-нибудь вам потребуется изменить то, как едят все позвоночные, хамелеоны получат изменения автоматически. Код полностью совпадает с кодом из базового класса. Нам действительно нужны две одинаковые версии одного кода ? Нельзя просто воспользоваться записью «Eat(morsel)», потому что будет вызвана реализация Chameleon.Eat. Для обращения к Vertebrate.Eat необходимо использовать ключевое слово «base».
дальше 4 347 н аслед ов ание Если базовый класс содержит конструктор, ваш субкласс должен его вызвать Вернемсяккоду, написанномус классамиBird,Pigeon, Ostrich иEgg.Мыхотимдобавить классBrokenEgg, которыйрасширяетEgg; с ним 25%яиц, которыеоткладываютголуби, оказываются разбитыми. Заме- ните новуюкоманду вPigeon.LayEgg следующей командойif/else, которая создает новый экземпляр Egg или Bro kenE gg: if (Bird.Randomizer.Next(4) == 0) eggs[i] = new BrokenEgg(Bird.Randomizer.NextDouble() * 2 + 1, "white"); else eggs[i] = new Egg(Bird.Randomizer.NextDouble() * 2 + 1, "white"); Осталось написать класс BrokenEgg,расширяющий Egg. Онбудет идентичен классу Egg, не считая того , что у него есть конструктор, выводящийна консоль сообщение(о том,что яйцо разбито): class BrokenEgg : Egg { public BrokenEgg() { Console.WriteLine("A bird laid a broken egg"); } } Внеситеэти два изменения впрограммуEgg.Ой-ой,кажется, следующиедве строки кода порождают ошибки компиляции: Ì Перваяошибка встроке,вкоторойсоздаетсяновый объектBrokenEgg: CS1729 - 'BrokenEgg' не содержит конструктора, который получает два аргумента. Ì Втораяошибка в конструктореBrokenEgg: CS7036 — не задан аргумент, соответствующий обяза- тельному формальному параметру 'size' для 'Egg.Egg(double, string)'. Вам предоставляется еще одна отличная возможность прочитать описания ошибок и попытаться по- нять, что же пошло не так. Первая ошибка достаточно очевидна: команда, которая создаетэкземпляр BrokenEgg, пытается передать конструктору два аргумента, но класс BrokenEgg имеет конструктор без параметров. Добавьте параметры в конструктор: public BrokenEgg(double size, string color) Перваяошибка исчезает —методMainнормальнокомпилируется.Какнасчет другойошибки?Разделим описание ошибки на несколько частей: Ì В описанииговорится о Egg.Egg(double, string), т. е. о конструктореклассаEgg. Ì В немупоминаетсяпараметр 'size',необходимый классуEggдля задания свойстваSize. Ì Ноаргумент незадан, потомучтонедостаточно изменить конструкторBrokenEgg дляполученияар- гументов, соответствующихпараметрам.Также необходимо вызватьконструкторбазовогокласса. ИзменитеклассBrokenEgg и включитевнегоключевое словоbaseдля вызоваконструкторабазовогокласса: public BrokenEgg(double size, string color) : base(size, color) Наконец код нормально компилируется. Попробуйте запустить его — теперь, когда экземпляр Pigeon откладываетяйцо, приблизительно в четверти случаев при создании экземплярабудет выводиться со- общение о том,что яйцо разбито(но последующийвывод остаетсянеизменным). Добавьте! Простой возврат к ста- ройверсиипроекта. Чтобы загрузить в IDE предыдущую версию проекта, выберите команду Recent Project and Solutions (Windows) илиRecent Solutions(Mac)изменюFile. РЕЛАКС
348 глава 6 ко нстр уир ов ание суб кл асса Субкласс и базовый класс могут иметь разные конструкторы КогдамыизменялиBrokenEggдля вызоваконструкторабазового класса,мыпривелиего конструктор в соответствие сбазовымклассомEgg.Аесли мыхотим, чтобывсеразбитые яйца имелинулевойразмер, аих цвет начиналсясослова «broken»?Измените команду, создающуюэкземплярBrokenEgg,чтобы передавалсятолько аргументдляцвета: if (Bird.Randomizer.Next(4) == 0) eggs[i] = new BrokenEgg("white"); else eggs[i] = new Egg(Bird.Randomizer.NextDouble() * 2 + 1, "white"); При внесении этого изменения вы снова получите ошибку компиляции, в которой говорится об «обя- зательном формальном параметре», — и это логично, потому что конструкторBrokenEgg имеет два параметра,но ему передается только одинаргумент. Исправьте свойкод — изменитеконструкторBrokenEgg,чтобыонполучал один параметр: class BrokenEgg : Egg { public BrokenEgg(string color) : base(0, $"broken {color}") { Console.WriteLine("A bird laid a broken egg"); } } Теперь снова запустите программу. Конструктор BrokenEgg по-прежнему выводитсвое сообщениена консоль в цикле for в конструктореPigeon,но теперь он также заставляет Eggинициализировать поляSize иColor. Когда циклforeachвметоде Mainвыводит наконсоль значение egg.Description, для каждого разбитого яйца выводится сообщение: Press P for pigeon, O for ostrich: p How many eggs should it lay? 7 A bird laid a broken egg A bird laid a broken egg A bird laid a broken egg A 2.4cm white egg A 0.0cm broken White egg A 3.0cm white egg A 1.4cm white egg A 0.0cm broken White egg A 0.0cm broken White egg A 2.7cm white egg Измените! Ко нст рукт ор суб кл асса может получать любое количество параметров, но может не получать ни одного. Необходимо лишь использовать ключевое слово base для передачи правиль- ного количества аргу- ментов конструктору базового класса. Какая погода там наверху? А вы знали, что голуби обычно откладывают только одно или два яйца? Как бы вы изменили класс Pigeon с учетом этого факта?
дальше 4 349 н аслед ов ание Так классы SwordDamage и ArrowDamage выглядели в начале этой главы.Они хорошо инкапсулированы, но большая часть кода SwordDamage дублируется вArrowDamage. Хороший код начинает формироваться вголове, аневIDE. Азначит, стоитвыделить времяна проектирование модели классов набумаге до того, какпереходить к про- граммированию. Мы ужезаписали именаклассов.Вамостаетсядобавитькомпонентывовсетрикласса и соединить прямоугольники стрелками. Длясправки мыприводимдиаграммы классовSwordDamageи ArrowDamage,которые были построены ранее. В каждый класс включен приватный метод CalculateDamage. Проследите за тем, чтобы при заполнении диаграммы классов были включены всеот- крытые, приватные и защищенные компоненты классов. Укажите модификатор досту- па (public,privateили protected)рядом с каждым компонентом класса. Пора доделать приложение для Оуэна Вэтойглаве все началосьс измененияпрограммы— калькулятораповреждений, когда-то построенного для Оуэна, чтобыпрограмма могла вычислять повреждениякак отмеча, такиот стрелы. Решениеработало, аклассыSwordDamage иArrowDamageбылихорошо инкапсулированы. Но кроме нескольких строк кода два класса были почти идентич- ны. Вы узнали, что повторение кода в разных классах неэффективно и небезопасно, особенно есливысобираетесь идалеерасширять программу,добавляявнееклассы для новыхвидоворужия.Теперь ввашемарсенале появилсяновыйинструмент для решения проблемы:наследование. Азначит, пришло время завершить приложение-калькулятор. Этобудет сделано за два шага: сначала новая модель классов будет спроектирована на бумаге, а затем мыреализуем ее вкоде. Построение модели классов на бумаге перед написанием кода помогает лучше понять суть проблемы и решить ее более эффективно. WeaponDamage ArrowDamage SwordDamage SwordDamage public Roll publicMagic public Flaming publicDamage private CalculateDamage ArrowDamage public Roll publicMagic public Flaming publicDamage private CalculateDamage Возьми в руку карандаш
350 глава 6 разделение обязанностей понижает сложность WeaponDamage public Roll public Magic public Flaming public Damage protected virtual CalculateDamage Минимальное перекрытие между классами ¦ важный принцип проектирования, называемый разделением обязанностей Еслисегоднявыхорошоспроектируетесвоиклассы,вамбудет проще изменять ихв будущем. Представьте, чтоу васдесятокразныхклассовдлявычисленияповрежденийотразныхвидоворужия. Что, есливы за- хотите изменить тип Magic сbool наint, чтобывигре моглоприсутствовать оружиесразнымибонусами (например,волшебнаябулава +3или волшебный кинжал+1)?Снаследованиемвампришлосьбыизменять свойствоMagicв суперклассе.Конечно, вам придетсяизменитьметодCalculateDamage длякаждого класса, но объемработыбудет намного меньше ипропадает рисктого, что вы случайно забудете изменить один из классов. (Впрофессиональнойразработке такое случаетсясплошь ирядом!) Этопример разделения обязанностей, потому что каждый класс содержит только код, относящийся к однойконкретнойчасти проблемы, решаемой вашейпрограммой.Код, относящийсятолько к мечам, размещаетсяв классеSwordDamage; код, относящийсятолькокстрелам, — в классе ArrowDamage;а общий кодвключаетсявкласс WeaponDamage. Припроектированииклассовразделение обязанностейдолжно стать однимиз первоочередных факто- ров, которые выдолжныучитывать.Еслина один классвозложены две разные обязанности, попробуйте разбить его на два разныхкласса. Помните: любую про- грамму можно написать множеством разных способов, и обычно «един- ственно правильного» ответа не существует — даже если этот ответ приведен в книге! Но если вам удалось найти ре- шение, не уступающее этому, в следующем упражнении мы будем придерживаться этой модели классов. SwordDamage protected override CalculateDamage ArrowDamage protected override CalculateDamage Над этой ситуацией стоит подумать. Мы выделили обязанности, относя- щиеся к пользовательскому вводу, в класс Program(а конкретно метод Program.Main). Сам по себе он не выполняет никаких вычислений — они инкапсулируются в методах CalculateDamage внутри классов SwordDamage и ArrowDamage. Но мы решили, что генерирование слу- чайных чисел для бросков кубиков относится к обязанностям метода Main, а не к обязанностям классов оружия. Было ли это правильным реш ени ем? Swor dD amage и A rr owD amage обладают одинаковыми свой- ствами. Логично вынести эти свойства в суперкласс W eapo nD amage. Ранее в этой главе классы были инкап- сулированы, для чего метод CalculateDamage был объявлен приват- ным. Так как субклас- сы должны обращать- ся к нему, необходимо заменить модификатор доступа на protected. Метод CalculateDamage помечен ключевым словом virtual, по- этому свойства вызывают его так же, как прежде. Так как он пере- определяется субклассами, при вызове WeaponDamage.Roll из объектаSwordDamage свойство вызывает метод CalculateDamage, определенный в SwordDamage. Возьми в руку карандаш Решение
дальше 4 351 н аслед ов ание Итак, вы спроектировали модель классов, и мы можем перейти к написанию кода ее реализации. Это оченьполезная привычка — сначала проектируйте свои классы, а затем преобразуйте их в код. Нижеописанапоследовательностьдействий по завершениюработыдля Оуэна. Вы можете снова от- крыть проект, созданный в начале главы, или же создать новый проект и скопировать в него нужные части. Если ваш код сильно отличается от кода решения, приведенного ранее в этой главе, мы рекомендуемначатьскода решения. Если вы не хотите вводить его вручную, загрузите его по адресу https://gitgub.com/head-first-csharp/fourth-edition. 1. Ничего не изменяйте в методе Main. В нем будут использоваться классы SwordDamage и ArrowDamage (так же, как в начале главы). 2. Реализуйте класс WeaponDamage. Добавьте новый класс WeaponDamage и приведите его в соответствие с диа- граммой классов из раздела «Возьми в руку карандаш». При этом необходимо учитыватьряд факторов: Ì Свойства вWeaponDamage почти идентичны свойствам в классахSwordDamage иArrowDamage в начале главы. Изменилось только одно ключевое слово. Ì Не включайте код в метод CalculateDamage (можно включить комментарий: /* переопределяется в субклас- се */). Метод должен быть виртуальным, он не может быть приватным, в противном случае произойдет ошибка компиляции): Ì Добавьте конструктор, который реализует начальный бросок. 3. Реализуйте класс SwordDamage. При этом необходимо учитывать несколько факторов: Ì Конструктор получает один параметр, который передается конструктору базового класса. Ì C# всегда вызывает наиболее конкретную реализацию метода. Это означает, что вы должны переопределить метод CalculateDamage и вычислить в нем повреждения от меча. Ì Также стоит задуматься над тем, как работает CalculateDamage. Set-методы свойств Roll, Magic и Flaming вызы- вают CalculateDamage, чтобы обеспечить автоматическое обновление поля Damage. Так как C# всегда вызывает наиболее конкретную реализацию метода, они вызовутSwordDamage.CalculateDamage, несмотря на то что они являются частью суперкласса WeaponDamage. 4. Реализуйте классArrowDamage. Он работает точно так же, как SwordDamage, если не считать того , что метод CalculateDamage вычисляет повреждения для стрелы, а не для меча. Мы можем внести довольно серьезные изменения в механизмы работы классов без изменения метода Main, из которого эти классы вызываются. Если ваши классы хорошо инкапсулированы, это значительно упро ща ет из ме нение кода. Еслиувасесть знакомый профессиональныйразработчик, спросите,какая задача за последнийгодвызвала унегобольше всегонегатива. С довольно высокой вероятностью он ответитчто-то вроде: «Мненадо было внести изменения в класс, но для этого пришлось изменить два другихкласса, что потребовало ещетрех изменений и т. д., — дажепросто отслеживать все эти изменения было достаточно трудно». Проектирование классов с учетом правилинкапсуляции поможетизбежать подобных ситуаций. Упражнение
352 глава 6 Ниже приведен код класса WeaponDamage. Свойства почти иден- тичны свойствам старых классов SwordDamage и ArrowDamage. Также появился конструктор для моделирования исходного броска кубиков и метод CalculateDamage, переопределяемый в субклассах. class WeaponDamage { public int Damage { get; protected set; } private int roll; public int Roll { get { return roll; } set { roll = value; CalculateDamage(); } } private bool magic; public bool Magic { get { return magic; } set { magic = value; CalculateDamage(); } } private bool flaming; public bool Flaming { get { return flaming; } set { flaming = value; CalculateDamage(); } } protected virtual void CalculateDamage() { /* Переопределяется субклассом */ } public WeaponDamage(int startingRoll) { roll = startingRoll; CalculateDamage(); } } WeaponDamage public Roll public Magic public Flaming public Damage protected virtual CalculateDamage Set-метод свойства Damageдолжен быть снабжен пометкой protected. Таким образом он будет досту- пен для субклассов, но никакой другой класс задать значение свойства не сможет. Так как свойство защищено от случайной записи из другихклассов, субклассы остаются хорошо инкапсулированными. Свойства могут вызвать метод CalculateDamage, который обнов- ляет свойство Damage. И хотя они определяются в суперклассе, при наследовании субклассом они вызывают метод CalculateDamage, определенный в этом субклассе. Полная аналогия с тем, как работал класс JewelThief, когда вы переопре- деляли метод из Locksmith. Это было нужно, чтобы вор крал бриллианты из сейфа, а не возвращал их владельцу. Метод CalculateDamage сам по себе пуст — мы поль- зуемся тем фактом, что C# всегда вызывает наибо- лее конкретную реализацию. В новой иерархии класс SwordDamage расширяет WeaponDamage, когда set- метод его унаследованного свойстваFlaming вызывает CalculateDamage, будетвыполненасамая конкретная реализация этого метода, поэтому вместо этого вызыва- ется реал и зац ия Swor dD amage. Cal cul at eDamag e. Упражнение Решение отладчик поможет понять
дальше 4 353 н аслед ов ание Класс SwordDamage расширяет WeaponDamage и переопределяет его метод CalculateDamage, чтобы реализовать вычисление повреждений от меча. Код выглядит так: class SwordDamage : WeaponDamage { public const int BASE_DAMAGE = 3; public const int FLAME_DAMAGE = 2; public SwordDamage(int startingRoll) : base(startingRoll) { } protected override void CalculateDamage() { decimal magicMultiplier = 1M; if (Magic) magicMultiplier = 1.75M; Damage = BASE_DAMAGE; Damage = (int)(Roll * magicMultiplier) + BASE_DAMAGE; if (Flaming) Damage += FLAME _DAMAGE; } } Перейдем к классу ArrowDamage. Он работает практически так же, как класс SwordDamage, не считая того , что он вычисляет повреждения для стрелы: class ArrowDamage : WeaponDamage { private const decimal BASE_MULTIPLIER = 0.35M; private const decimal MAGIC_MULTIPLIER = 2.5M; private const decimal FLAME_DAMAGE = 1.25M; public ArrowDamage(int startingRoll) : base(startingRoll) { } protected override void CalculateDamage() { decimal baseDamage = Roll * BASE_MULTIPLIER; if (Magic) baseDamage *= MAGIC_MULTIPLIER; if (Flaming) Damage = (int)Math.Ceiling(baseDamage + FLAME_DAMAGE); else Damage = (int)Math.Ceiling(baseDamage); } } SwordDamage protected override CalculateDamage ArrowDam age protected override CalculateDamage От конструктора требуется совсем немно- го: вызвать конструктор суперкласса при помощи ключевого слова base и передать ему в аргументе параметр startingRoll. Отладчик поможет понять, как работают эти классы Одна из важнейших идей этой главызаключается в том,что прирасширении классаможно переопределитьегометоды, чтобывнестивесьма значительные изменения вего поведение. Чтобы понять,какработаетэтотмеханизм,мывоспользуемсяотладчиком: Ì Установитеточки прерывания встрокахset-методов Roll,Magic иFlaming, вкоторыхвызывается CalculateDamage. Ì ДобавьтекомандуConsole.WriteLineвWeaponDamage.CalculateDamage. Эта команда никогда не выполняется. Ì Запустите программу.Когда онадостигает какой-либоизточекпрерывания, используйте команду Step Into для входа в метод CalculateDamage. Управление передаетсяреализациисубкласса— метод WeaponDamage.CalculateDamage вызываться небудет. Упражнение Решение Сделайте это!
354 глава 6 механика, динамика, эстетика Мы собираемся обсудить важный элемент разработки игр: динами- ку. На самом деле эта концепция настолько важна, что она отнюдь не ограничивается играми. Собственно, динамика в какой-то степени проявляется практически во всех видах приложений. Динамика Динамикаигры описывает, каким образом игровые механики комбинируются и взаимодействуют для управления игро- вым процессом. В любой ситуации, где есть игровые механики, они естественным образом формируют динамику. Кон- цепция не ограничивается видеоиграми — механика присутствует во всех играх, а динамики возникают из всех механик. • Хороший пример механики уже встречался нам ранее: в ролевой игре Оуэна для вычисления повреждений от разных видов оружия использовались формулы (встроенные в классы вычисления повреждений). Это хорошая от- правная точка для анализа того, как изменения в механике влияют на динамику. • Что произойдет, если вы измените механикуформулыдля стрелы, чтобыбазовые повреждения умножались на 10? Это незначительное изменение в механике приводит к огромным изменениям вдинамике игры. Внезапно стрелы становятся намного более смертоносными, чем мечи . Игроки перестанут пользоваться мечами и начнут стрелять из лука даже на минимальном расстоянии — и это изменение динамики. • Когда поведение игроков изменится, Оуэн должен изменить сценарий кампании. Например, некоторые сражения, которые разрабатывались как сложные, внезапно становятся слишком простыми для игроков. Игроки снова меняют поведение. Ненадолго остановитесь и поразмыслите над происходящим. Крошечные изменения в правилах приводят к колос- сальным изменениям в динамике. Оуэн не вносил эти изменения непосредственно в игровой процесс; они стали побоч- ными эффектами этого небольшого изменения в правилах. С технической точки зрения изменениединамики проявилось в результате изменения механики. • Если вы еще не сталкивались с концепцией проявления, она может показаться немного странной, поэтому рассмо- трим конкретный пример из классической видеоигры. • Механика игры Space Invaders проста. Пришельцы двигаются влево-вправо и стреляют вниз; если выстрел по- падает в корабль игрока, то игрок теряет одну жизнь. Игрок перемещает свой корабль влево-вправо и стреляет вверх. Если выстрел попадает в пришельца, этот пришелец уничтожается. Время от времени у верхнего края экрана про- летает командный корабль; его уничтожение приносит дополнительные очки игроку. Энергетическая защита посте- пенно ослабевает от выстрелов. За пришельцев разных видов игрок получает разное количество очков. С течением времени пришельцы начинают двигаться быстрее... В общем-то все. • С динамикой игры Space Invaders дело обстоит сложнее. Поначалу игра проста:большинство игроков справляется с первой волной нападения, но она быстро усложняется. Изменяется только скорость перемещения пришельцев. На- растание скорости пришельцев изменяет всю игру. Темп, т . е . субъективное восприятие скорости игры, радикально изменяется. • Некоторые игроки стараются уничтожать пришельцев от края построения, потому что наличие «разрывов» замедляет снижение. Этот момент нигде не отражен в коде, в котором есть только простые правила перемещения пришельцев. Он относитсяк динамике и является побочным эффектом сочетания механик, а именно механики работы выстрелов с правилами перемещения пришельцев. Ничто из этого не запрограммировано в коде игры. Это не часть игровой механики, а динамика. На первый взгляд динамика может показаться какой-то абстрактной концепцией. Позднее в этой главе мы еще займемся этой темой, а пока просто помните обо всем, что было сказано о динамике, во время работы над следующим проектом. Сможете ли вы заметить, как динамика начинает влиять на игру, в процессе программирования? Разработка игр... и не только
дальше 4 355 н аслед ов ание Знаете что? Я уже по горло сыта играми. Игры на поиск пар, 3D-игры, игры с числами, классы для карт и пейнтбольных маркеров, которые должны использоваться в играх, модели классов для игр, проектирование игр... Получается, мы не занимаемся ничем, кроме игр. Да, мы все знаем, что разработчики C# могут найти очень неплохие вакансии на рынке труда. Я хочу изучить C#, чтобы использовать его в серьезной работе. Нельзя ли сделать хотя бы один проект, в котором мы займемся серьезным бизнес- приложением? Видеоигры — это серьезная область бизнеса. Индустрия видеоигр с каждым годом растет в глобальном масштабе. В ней работают сотни тысяч людей по всему миру, и талантливый разработчик непременно найдет себе в ней достойное место!Существует целая экосистема не- зависимых разработчиков, которые создают и продают игры— в одиночку илинебольшими группами. Новыправы — C# серьезный язык, и он используется для созданиямногихсерьезных приложений, не имеющихотно- шения киграм.Собственно,хотяC#сталлюбимымязыком многих разработчиков игр, онтакже стал одним из самых популярных языковво многих другихотраслях. Поэтому чтобы немного потренироваться в применении наследования, вследующем проектемы создадим серьезное бизнес-приложение.
356 глава 6 Построение системы управления ульем Теперьвашапомощьнужнапчелинойматке!Улейвышелиз-под контроля, и ей нужна программа, котораяпоможетуправлять процессом производства меда. Улей полон рабочих пчел, имеется и список заданий. Нужно распределить задания между пчелами с учетом их специализации. Постройте си- стему,управляющую поведением рабочих пчел. Вот как она должна функционировать: Матка раздает задания рабочим. Существует три вида работ. Некоторые пчелыумеют вылетать из улья и приносить в него нектар. Другие перерабатывают нектар в мед, которым питаются пчелы. Наконец, матка постоянно откладывает яйца, а пчелы-опекуны следят за тем, чтобы из яиц выросли новыерабочие. Когда все задания будут распределены, время работать. Раздав задания, матка заставляет пчел отработать очередную смену щелчком на кнопке «Work the next shift» в приложении. Программа строит отчетс информацией о том, сколько пчел еще работает над каждой задачей,а такжео количественектара имеда в хранилище. Управление ростом улья. Какивсеруководителибизнеса, пчелиная матка стремится красширению своего предприятия. Улей— сложнаясистема,иматка оцениваетего размер по общему количествурабочих. Какбывы реализовали возможность добавления новых рабочих? До какого размера может вырасти улей, преждечем внем кончитсямед ипредприятие обанкротится? 1 2 3 пчелиный труд Версия этого проекта для Mac доступна в приложении «Visual Studio для пользователей Mac».
дальше 4 357 н аслед ов ание NectarCollector overridefloatCostPerShift protec ted over ri de DoJ ob HoneyManufacturer overridefloatCostPerShift protec ted ov err ide DoJ ob Egg Car e overridefloatCostPerShift pr otected ov err ide D oJob Queen s tri ng S tatus Report (read-only) override floatCostPerShift private Bee[] workers AssignBee CareF orE ggs protec ted over ri de DoJ ob Bee stringJob virtual floatCostPerShift (read-only) W ork The NextS hi ft pr otected v i rtual DoJ ob MainWindow pr iv ate Queen quee n WorkShift_Click AssignJob_Click staticHoneyVault s tri ng S tatus Report (read-only) privatefloathoney =25f privatefloat nectar = 100f Coll ec tNectar Conver tNec tarToHoney bool Cons umeHoney Модель классов системы управления ульем Нижепредставлены классы,которые мы построим длясистемыуправленияульем. Будет использоваться модель с базовым классом и четырьмя субклассами, статический класс для управления объемами меда инектара,а такжекласс MainWindow,содержащий кодпрограммнойчасти главного окна. HoneyVault — статический класс, который отслеживает теку- щие объемы меда и нектара в улье. Пчелы используют метод ConsumeHoney, который проверяет, достаточно ли меда для выполне- ния их задач, и если достаточно, — вычитает запрашиваемый объем. Bee — базовый класс для всех классов пчел. Его метод WorkTheNextShift вызывает метод ConsumeHoney класса HoneyVault, и если тот вернет true, вызывает DoJ ob. Код программной части главного окна решает не- сколько задач. Он создает экземпляр Queen и обра- ботчики события Click для кнопок, которые вызывают методы WorkTheNextShift и AssignBee и выводят от- чет. Этот субкласс Bee исполь- зует массив для отслежи- вания рабочих и переопре- деляет DoJob для вызова их методов WorkTheNextShift. Этот подкласс Bee переопределяет doJob для вызова метода HoneyVault для сбора нектара. Этот субкласс Bee переопределяет DoJob для вызова метода HoneyVault, преобра- зующего нектар в мед. Этот субкласс Bee хранит ссылку на Queen и переопреде- ляет DoJob для вызова метода CareForEggs класса Queen. Этамодель классов — всего лишь начало. Мы приведем больше информации в процессе написания кода. Оченьвнимательнопроанализируйтеэтумодель классов. Онасодержит много ценной информацииоприложении,котороевам предстоит построить. Затеммыприведемвсю информа- цию,необходимую для написания кода этих классов. РЕЛАКС
358 глава 6 вн утри си стемы упр авлен ия ул ьем Ì Затем цикл foreach используется для вызова метода WorkTheNextShift. Ì Все незанятыерабочие потребляют мед. Потреб- ляемое количество определяется константой HONEY_PER _UNASSIGNED _WORKER . Ì Наконец, он вызывает свой метод UpdateSta- tusReport. Когда вы нажимаете кнопку, чтобыназначить за- дание пчеле, обработчик события вызывает метод AssignBeeобъектаQueen. Методполучаетстро- ку с названием задания(котораячитаетсяиз jobSelector.text). Он использует команду switch для создания нового экземпляра соответствующего субкласса Bee ипере- дачи его методу AddWorker, поэтому добавьте метод AddWorker вкласс Queen. О б ъ е к т M a inWi n d o w Bee Bee Bee /// <summary> /// Расширяет массив workers на один элемент и добавляет ссылку Bee. /// </summary> /// <param name="worker">Рабочий, добавляемый в массив workers.</param> private void AddWorker(Bee worker) { if (unassignedWorkers >= 1) { unassignedWorkers--; Array.Resize(ref workers, workers.Length + 1); workers[workers.Length - 1] = worker; } } О б ъ е к т Que e n М а сси в B e e E g g C a r e N e c t a r C o l lect o r Класс Queen: как матка управляет рабочими Когда вы нажимаетекнопку для отработки следующей смены,обработчик событияClick кнопки вы- зывает метод WorkTheNextShift объекта Queen, унаследованный отбазового класса Bee. Вотчто про- исходитпослеэтого: Ì Bee.WorkTheNextShiftвызываетHoneyVault.ConsumeHoney(HoneyConsumed), используясвойство CostPerShift(которое переопределяетсяразнымизначениямивсубклассах) дляопределениятого, сколько меда потребуетсядляработы. Ì Затем Bee.WorkTheNextShift вызывает DoJob, который также переопределяется Queen. Ì Queen.DoJobувеличивает свое приватное поле eggs на0.45 (с использованием константы EGGS_PER _SHIFT). Пчела EggCare вызывает свой метод CareForEggs, который уменьшает eggs и увеличивает unassignedWorkers. Длина экземпляра Array не может изменяться на про- тяжении его жизненного цикла. Именно по этой при- чине в C# существует полезный статический метод Array.Resize. Он не изменяет размер массива. Вместо этого он создает новый экземпляр и копирует в него содержимое старого. Обратите внимание на ключевое слово ref — вы еще узнаете о нем позднее в книге. Метод AddWorker добавляет ново- го рабочего в массив рабочих класса Queen. Он вызывает Array. Resize для расшире- ния массива, а затем добавляет в него нового рабочего. H o n e y M a n ufa c t u r e r
дальше 4 359 н аслед ов ание <Grid> <Grid.RowDefinitions> <RowDefinition Height="1*"/> <RowDefinition Height="4*"/> <RowDefinition Height="3*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Label Content="Job Assignments" FontSize="18" Margin="20,0" HorizontalAlignment="Center" VerticalAlignment="Bottom"/> <StackPanel Grid.Row="1" VerticalAlignment="Top" Margin="20"> <ComboBox x:Name="jobSelector" FontSize="18 " SelectedIndex="0 " Margin="0,0,0,20"> <ListBoxItem Content="Nectar Collector"/> <ListBoxItem Content="Honey Manufacturer"/> <ListBoxItem Content="Egg Care"/> </ComboBox> <Button Content="Assign this job to a bee" FontSize="18px" Click="AssignJob_Click" /> </StackPanel> <Button Grid.Row="2" Content="Work the next shift" FontSize="18px" Click="WorkShift_Click" Margin="20"/> <Label Content="Queen's Report" Grid.Column="1" FontSize="18" Margin="20,0" VerticalAlignment="Bottom" HorizontalAlignment="Center"/> <TextBox x:Name="statusReport" IsReadOnly="True" Grid.Row="1 " Grid.RowSpan="2 " Grid.Column="1 " Margin="20"/> </Grid> Т р и с т р о к и с в ы с о т а м и ( н и з ) 3 * 4 * 1 * ( в е р х ) Сетка с двумя столбцами равной ширины Пользовательский интерфейс: добавление кода XAML главного окна Создайте новое приложение WPF с именем BeehiveManagementSystem. Структура макета главного окна определяется сеткойсо свойствамиTitle = "Beehive Management System" Height="325" Width="625". В макетеиспользуются теже элементыLabel,StackPanel иButton, которыевстречались вам впредыду- щихглавах, а такжедобавляютсядва новых элемента. Раскрывающийся списоквгруппеJobAssignments представляетсобой элементComboBox,вкотором пользователь можетвыбратьвариантиз списка. От- чет о текущем состоянии из раздела Queen’s Report выводится в элементе TextBox. Раскрывающийся список представляет собой элемент ComboBox. Это контейнерный элемент (как и Grid, например), у которого между откры- вающим и закрывающим тегами следуют элементы. В данном случае он содержит три элемента ListBoxItem, по одному для каждого вари- анта, который может быть выбран пользователем. Вообще говоря, вы можете раскрыть раздел Common в окне Properties и воспользоваться кно пкой рядом с пунктом Items (выберите в списке ListBoxItem), но на самом деле проще ввести определения элементов в XAML вручную. Проследите за тем, чтобы содержимое каждого элемента точно соответ- ствовало показанному ниже. Это элемент TextBox. Обычно TextBox используется для ввода данных пользователем, но здесь его свойству IsReadOnly задано значение True, что- бы элементбылдоступен только для чтения.Мы используем еговместо эл еме нта Tex t Blo ck , который испол ь- зовался в предыдущих проектах, по двум причинам. Во-первых, его гра- ница о бводитс я ра мко й, а это хорошо смотри тся . Во- вторы х, он п озволя е т выделять икопироватьтекст, что очень полезно для отчета о текущем состоянии в бизнес-приложении. Присвойте элементуTextBox имя (x:Name), чтобы иметь воз- можность задать его свойство Text в коде программной части. Элементы ListBoxItem определяют ва- рианты, которые пользователь видит в списке ComboBox.
360 глава 6 пчелы и бизнес Постройте систему управления ульем. Цель системы — максимизировать количество рабочих, которым поручено выполнениеразличных функций в улье, и поддерживать работу улья до тех пор, пока в нем не кончится мед. Правилаулья Рабочим может поручаться одно из трех заданий:сборщики нектара приносят нектарв хранилище, производителимеда преобразуют нектар в мед, а опекуны превращают яйца в рабочих, которым могут поручаться задания. Во время смены матка откладывает яйца (чуть меньшедвух сменуходит на отладывание одного яйца).Матка обновляет свой отчет текущегосостояния в конце каждой смены. В отчете приводится информация о заполнении хранилища, количество яиц, нераспределенных рабочих и пчел, назначенных для каждого видаработы. Начнитес построения статического классаHoheyVault • Класс HoneyVault станет хорошей отправной точкой, потому что он не имеет зависимостей, иначе говоря, он не вызывает мето- ды инеиспользует свойства или поля других классов. Начните с создания нового класса HoneyVault.Объявите его с ключевым словом static, затем обратитесь кдиаграмме класса идобавьте компоненты класса. • HoneyVault содержит две константы (NECTAR_CONVERSION_RATIO = .19fи LOW_LEVEL _WARNING = 10f), которые использу- ются в методах. Приватное полеhoney инициализируется значением25f, а приватное поле nectar — значением 100f. • Метод ConvertNectarToHoney преобразует нектар в мед.Он получает параметрfloat с именем amount,уменьшает поле nectar на указанную величину иувеличивает поле honey на величину amount ×NECTAR_CONVERSION_RATIO.(Еслизапрашиваемое значение больше количества нектара, оставшегося в хранилище, то преобразуется весь оставшийся нектар.) • Метод ConsumeHoney определяет то, как пчелы потребляют мед для выполнения своих задач. Метод получает параметр amount. Если он меньше текущего содержимого поля honey, то метод вычитает amount из honey и возвращаетtrue; в противном случае возвращается false. • Метод CollectNectar вызывается пчелой NectarCollector каждую смену.Метод получает параметр amount. Если значение пара- метра положительное, оно прибавляется к полю honey. • Свойство StatusReport имеет только get-метод доступа, который возвращает строку с количеством меда и нектара в хранили- ще. Если количество меда ниже порогаLOW_LEVEL_WARNING,добавляется предупреждение ("LOW HONEY —ADDA HONEY MANUFACTURER") .То же самое происходит и с полем nectar. Создайте классBee и начинайте строить классы Queen, HoneyManufacturer, NectarCollector и EggCare. • Создайте базовый класс Bee. Его конструктор получает строку, которая используется для присваивания свойства Job, до- ступного толькодля чтения. Каждый субкласc Bee передает строку своему базовому конструктору — "Queen", "NectarCollector", " Honey Manufacturer" или "Egg Care", — так чт о кла сс Queen содержит следующий код: public Queen() :base("Queen") . • Виртуальное и доступное только для чтения свойство CostPerShift позволяет каждому субклассу Bee определить количество меда, потребляемого им за смену.Метод WorkTheNextShiftпередаетHoneyConsumed методу HoneyVault.ConsumeHoney.Если ConsumeHoney возвращаетtrue, значит , в улье остается достаточно меда иWorkTheNextShiftвызываетDoJob. • Создайте пустые классы HoneyManufacturer, NectarCollector и EggCare, которые просто расширяют Bee, — они п отребуются вам для построения классаQueen. Сначала мыдостроим классQueen, а затем вернемся и доделаемдругиесубклассыBee. • Каждый субклассBee переопределяет методDoJob кодом,реализующим егозадание,и переопределяет свойствоCostPerShift количеством меда, потребляемым за смену. • Нижеприведены значения свойства Bee.CostPerShift,доступного только для чтения, для каждого субклассаBee: • Q ueen.Co stP er S hift в озв ра щает 2.15f, Nec tarC ol lec tor.C ostP er S hi ft в оз в ращает 1.95f, Hone yMan ufact urer.C ost Pe rS hi ft в оз в ращает 1.7f, и E ggCar e.Cos tP erS hi ft в оз в ращает 1.35f. Упражнение большое, но не падайте духом! Просто разбейте его на мень- шие части. А когда вы начнете работать над ними, вы поймете, что многое из этого вам уже знакомо. Каждая отдельная часть этого упражнения вам уже знакома. Она вам ПО СИЛАМ! Длинные упражнения
дальше 4 361 н аслед ов ание Упражнение получилось длинным, но это нормально!Простостройте его класс за классом. Сна- чала постройте класс Queen. Когда это будет сделано, вернитесь к другим субклассам Bee. • Класс Queen содержит приватное полеBee[] с именем workers. Изначально массив пуст. Мы при- вели метод AddWorkerдля добавления в массив ссылок на Bee. • МетодAssignBee получает параметр с названием задания (например, "Egg Care").Онсодержит командуswitch(job)с ус- ловиями, в которых вызываетсяAddWorker. Например, если job содержит "Egg Care",то будетвызванметодAddWorker(new EggCare(this)). • Класс содержитдва приватных поля float с именами eggs и unassignedWorkers, в которых хранится количество яиц (добавляе- мых при каждой смене) и количестворабочих, ожидающих назначения задания. • Класс Queen переопределяет метод DoJob для добавления яиц, отдает приказ рабочим пчелам о начале работы и кормит медом рабочих, еще не получивших назначения на работу. Константа EGGS_PER_SHIFT (которой присваивается 0.45f) при- бавляется к полю eggs. Циклforeach используется для вызова метода WorkTheNextShiftкаждого рабочего. Затем вызывается метод HoneyVault.ConsumeHoney, которому передается константаHONEY_PER_UNASSIGNED_WORKER(0.5f)× workers.Length. • Изначально матка располагает тремя свободными рабочими — ее конструктор вызывает метод AssignBee три раза, чтобы создать трех рабочих пчел, по одной каждого типа. • Пчелы-опекуныEggCareвызываютметодCareForEggsклассаQueen. Он получает параметрfloat с именемeggsToConvert.Если поле eggs >= eggsToConvert, то значение eggsToConvert вычитается из eggs и прибавляется к unassignedWorkers. • Внимательнопросмотрите отчет о текущем состоянии на снимке — он генерируется приватным методом UpdateStatusReport (сиспользованиемHoneyVault.StatusReport).Класс QueenвызываетUpdateStatusReport в концесвоих методов DoJob иAssignBee. Завершите построениедругих субклассов Bee • КлассNectarCollector содержит константу NECTAR_COLLECTED_PER_SHIFT=33.25f.Его методDoJob передает эту константу Honey Vau lt.C oll ec tNec tar. • КлассHoneyManufacturer содержит константу NECTAR_PROCESSED_PER_SHIFT = 33.15f.Его метод DoJob передает эту кон- с тант у H oney Vaul t.Con ver tNec tar ToH oney. • Класс EggCare содержит константу CARE_PROGRESS_PER_SHIFT = 0.15f. Его метод DoJob передает эту константу queen. CareForEggs, используя приватную ссылку Queen, которая инициализируется в конструктореEgCare. Построение кода программной части главного окна • Мыпредоставили вам кодXAML главного окна. Ваша задача — добавить код программной части. Он содержит приватное поле Queen с именем queen, которое инициализируется в конструкторе, и обработчики событий для кнопок и ComboBox. • Подключите обработчики событий. Кнопка «Assign Job» вызывает queen.AssignBee(jobSelector.Text). Кнопка «Work the Next Shift» вызывает queen.WorkTheNextShift.Обе кнопки присваивают statusReport.Textрезультатqueen.StatusReport. Подробнее о работе системы управления ульем • Ваша цель — добиться как можнобольшего количестварабочих с назначенными заданиями(строкаTOTALWORKERS в отчете о текущем состоянии), а это зависит от того , каких рабочих вы добавляете и когда это происходит. Рабочие потребляют мед; еслиу вас будет слишкоммного рабочих одного типа, то уровень меда начнет падать. В процессе выполнения программы следите за уровнями нектара и меда. После нескольких первых смен вы получите предупреждение о нехватке меда (поэтому добавьте производителя меда);еще после нескольких — предупреждение о нехватке нектара(поэтому добавьте сборщика нек- тара), а после этого вам, возможно, придется расширять персонал улья. Какого значения TOTALWORKERS вамудастся добить- ся, прежде чем в улье кончится мед? Длинные упражнения
362 глава 6 решен ие Этобольшой проект, и он состоит из многих частей.Если у вас возникнут затруднения, просто разде- лите текущую задачу на части. И это не волшебство — у вас уже есть все необходимоедля понимания всех без исключения частей. Код статического классаHoneyVault: static class HoneyVault { public const float NECTAR_CONVERSION _RATIO = .1 9 f; public const float LOW_LEVEL_WARNING = 10f; private static float honey = 25f; private static float nectar = 100f; public static void CollectNectar(float amount) { if (amount > 0f) nectar += amount; } public static void ConvertNectarToHoney(float amount) { float nectarToConvert = amount; if (nectarToConvert > nectar) nectarToConvert = nectar; nectar -= nectarToConvert; honey += nectarToConvert * NECTAR_CONVERSION _RATIO; } public static bool ConsumeHoney(float amount) { if (honey >= amount) { honey -= amount; return true; } return false; } public static string StatusReport { get { string status = $"{honey:0.0} units of honey\n" + $"{nectar:0.0} units of nectar"; string warnings = ""; if (honey < LOW_LEVEL _WARNING) warnings += " \nLOW HONEY - ADD A HONEY MANUFACTURER "; if (nectar < LOW_LEVEL _WARNING) warnings += " \nLOW NECTAR - ADD A NECTAR COLLECTOR "; return status + warnings; } } } Пчелы NectarCollector выполняют свою работу, вызывая метод CollectNectar, чтобы добавить нектар в улей. Константы из классаHoneyVault очень важны. Попробуйте повы- сить коэффициент преобразования нектара в мед — и с каждой сменой в улье будет появляться много меда. Попробуйте его уменьшить — и почти сразу же столкнетесь с нехваткой меда. Пчелы HoneyManufacturer вы- полняют свою работу, вы- зывая ConvertNectarToHoney, что приводит к уменьше- нию количества нектара и увеличению количества меда в хранилище. Каждая пчела старает- ся потребить конкрет- ное количество меда за каждую смену. Метод ConsumeToHoney возвращает true только в том случае, если в улье хватает меда для выполнения его работы. Если ваш код не совпадает с нашим полностью, это нормально! Такие задачи могут решаться многими разными способами, и чем больше программа, тем больше возможных вариан- тов ее написания. Если ваш код раб отает, зн ачит, уп раж- нение решено правильно! Но выделите несколько минут на то, чтобы срав- нить ваше решение с на- шим, и попробуйте понять, почему мы приняли именно те, а не иные решения. Попробуйте воспользоваться меню Viewдля отображения в IDE представления Class View (панель будет пристыкована в окне Solution Explorer). Это полезный ин- струментдля исследования иерархии классов. Попробуйте раскрыть класс в окне Class View, затем разверните папкуBaseTypesи просмотрите ее иерархию. Для переключения междупредставлением Class View и Solution Explorer используются вкладки в нижней части окна. Решение длинных упражнений
дальше 4 363 н аслед ов ание Поведение этой программы определяется тем, как составляющие ее классы взаимодействуют друг с другом, особенно классы из иерархииBee. На вершине этой иерархиирасполагается суперклассBee, расширяемый всеми остальными классамиBee: class Bee { public virtual float CostPerShift { get; } public string Job { get; private set; } public Bee(string job) { Job = job; } public void WorkTheNextShift() { if (HoneyVault.ConsumeHoney(CostPerShift)) { DoJob(); } } protected virtual void DoJob() { /* the subclass overrides this */ } } КлассNectarCollector каждую смену собирает нектар и приносит его в хранилище: class NectarCollector : Bee { public const float NECTAR_COLLECTED _PER _SHIFT = 33 .25f; public override float CostPerShift { get { return 1.95f; } } public NectarCollector() : base("Nectar Collector") { } protected override void DoJob() { HoneyVault.CollectNectar(NECTAR_COLLECTED _PER _SHIFT); } } КлассHoneyManufacturer преобразует нектар, находящийся в хранилище, в мед: class HoneyManufacturer : Bee { public const float NECTAR_PROCESSED _PER _SHIFT = 33 .15f; public override float CostPerShift { get { return 1.7f; } } public HoneyManufacturer() : base("Honey Manufacturer") { } protected override void DoJob() { HoneyVault.ConvertNectarToHoney(NECTAR_PROCESSED _PER _SHIFT); } } Конструктор Bee получает один параметр, ко - торый используется для инициализации его свой- ства Job, доступного только для чтения. Класс Queen использует это свойство при построении отчета, чтобы определить, к какой разновидно- сти относится эта конкретная пчела. Классы NectarCollector и HoneyManufacturer опре- деляют константы, управ- ляющие тем, сколько не - ктара собирается и какая его часть преобразуется в мед при каждой сме- не. Попробуйте изменить их — к изменению этих констант программа куда менее чувствительна, чем к изменению коэффициен- та преобразования нектара в мед. Решение длинных упражнений
364 глава 6 решен ие Каждый из субклассов Bee выполняет свою работу, но все они обладают общим поведением, даже Queen. Все они работают по сменам, но выполняют работу только при наличии достаточного количе- ства меда. Класс Queen управляетрабочими и генерирует отчеты о текущем состоянии: class Queen : Bee { public const float EGGS_PER _SHIFT = 0.45f; public const float HONEY_PER _UNASSIGNED _WORKER = 0 .5f; private Bee[] workers = new Bee[0]; private float eggs = 0; private float unassignedWorkers = 3; public string StatusReport { get; private set; } public override float CostPerShift { get { return 2.15f; } } public Queen() : base("Queen") { AssignBee("Nectar Collector"); AssignBee("Honey Manufacturer"); AssignBee("Egg Care"); } private void AddWorker(Bee worker) { if (unassignedWorkers >= 1) { unassignedWorkers--; Array.Resize(ref workers, workers.Length + 1); workers[workers.Length - 1] = worker; } } private void UpdateStatusReport() { StatusReport = $"Vault report:\n{HoneyVault.StatusReport}\n" + $"\nEgg count: {eggs:0.0}\nUnassigned workers: {unassignedWorkers:0.0}\n" + $"{WorkerStatus("Nectar Collector")}\n{WorkerStatus("Honey Manufacturer")}" + $"\n{WorkerStatus("Egg Care")}\nTOTAL WORKERS: {workers.Length}"; } public void CareForEggs(float eggsToConvert) { if (eggs >= eggsToConvert) { eggs -= eggsToConvert; unassignedWorkers += eggsToConvert; } } К он стант ы к ласса Queen чрезвычайно важны, по- тому что они определяют поведение программы на протяжении нескольких смен. Если матка отло- жит слишком много яиц, они будут есть больше мед а, но т акже уско рят ход работ. Если рабочие, которым еще небыли на- значены задания, будут потреблять больше меда, это сделает актуальным бол ее бы стр ое наз наче- ние заданий. Изначально Queen на- значает на работу по одной рабочей пчеле каждого типа в своем конструкторе. Чтобы понять, ка - кая информация здесь должна выводиться, следует внимательно присмотреться к от- чету на снимке экрана. Пчелы EggCare вызывают метод CareForEggs для преобразования яиц в рабочих, которым еще не назна- чено задание. Мы предоставили вам метод AddWorker. Он изменяет размер массива и добавляет объект Bee в конец массива. А вы заметили, что иногда в отчете о текущем состоянии указано, что коли - чество рабочих, не имеющих задания, выводится как равное 1.0, но при этом добавить нового раб очег о не удает ся? Устан ов и- те точку прерывания в первую строку AddWorker — вы увидите, что з начен ие un assig ned Wor kers р авн о 0.9999999999999... См ож е- те ли вы предложить возможное решение этой проблемы? Решение длинных упражнений
дальше 4 365 н аслед ов ание Класс Queen управляет всей работой в программе — он отслеживает экземпляры ра- бочих Bee, создает новые экземпляры, когда возникает необходимость в назначении их на работу, и приказывает им начать отработку смен: private string WorkerStatus(string job) { int count = 0; foreach (Bee worker in workers) if (worker.Job == job) count++; string s = "s"; if(count==1)s=""; return $"{count} {job} bee{s}"; } public void AssignBee(string job) { switch (job) { case "Nectar Collector": AddWorker(new NectarCollector()); break; case "Honey Manufacturer": AddWorker(new HoneyManufacturer()); break; case "Egg Care": AddWorker(new EggCare(this)); break; } UpdateStatusReport(); } protected override void DoJob() { eggs += EGGS_PER _SHIFT; foreach (Bee worker in workers) { worker.WorkTheNextShift(); } HoneyVault.ConsumeHoney(unassignedWorkers * HONEY_PER _UNASSIGNED _WORKER); UpdateStatusReport(); } } Queen не занимается мелочным ад м ини стриро - ванием. Класс всего лишь позволяет объектам рабочих Bee выполнить свои задания и потре- бить свою долю меда. Мето д Assi gn Bee исп ол ьзует команду switch для определе- ния типа добавляемого рабо- чего. Строки в командах case должны точно соответствовать свойствуContent каждого эле- мента ListBoxItem в ComboBox, в противном случае ни один из вариантов не подойдет. Приватный метод WorkerStatus ис- пользует цикл foreach для подсчета в массиве пчел, выполняющих кон- кретное задание. Обратите внима- ние на использование переменной «s» для множественного числа «bees», если количество пчел больше 1. В ходе выполнения своей работы Queen до- бавляет яйца, приказывает каждому ра- бочему отработать следующую смену, а затем следит за тем, чтобы все свободные рабочие потребляли мед. Queen обновляет отчет о текущем состоянии после назна- чения каждой пчелы на работу и отработки смены, чтобы информация постоянно была акт уал ь ной. Хороший пример раз- деления обязанностей: поведение, относящееся к деятельности пчелиной матки, инкапсулирует- ся в классе Queen, а класс Bee содержит только поведение, общее для всех пчел. Решение длинных упражнений
366 глава 6 решен ие Константывначалекаждого из субклассовBee очень важны. Мы подбирализначения этих констант методом проб и ошибок: слегка изменяли одно из чисел, запускали программу и смотрели, к чему это приведет. Кажется, что нам удалось добиться неплохого баланса между классами. Как вы думаете, у нас получилось?А может, у вас получится лучше?Да почти наверняка! Класс EggCareиспользует ссылку на объектQueenдля вызова его методаCareForEggs с целью преобразования яиц в рабочих: class EggCare : Bee { public const float CARE_PROGRESS _PER _SHIFT = 0 .15f; public override float CostPerShift { get { return 1.35f; } } private Queen queen; public EggCare(Queen queen) : base("Egg Care") { this.queen = queen; } protected override void DoJob() { queen.CareForEggs(CARE_PROGRESS_PER _SHIFT); } } Ниже приведен код программной части для главного окна. Он делает не так много — вся содержательная работа выполняется в другихклассах: public partial class MainWindow : Window { private Queen queen = new Queen(); public MainWindow() { InitializeComponent(); statusReport.Text = queen.StatusReport; } private void WorkShift_Click(object sender, RoutedEventArgs e) { queen.WorkTheNextShift(); statusReport.Text = queen.StatusReport; } private void AssignJob_Click(object sender, RoutedEventArgs e) { queen.AssignBee(jobSelector.Text); statusReport.Text = queen.StatusReport; } } Помните: если у вас возникнут проблемы с написанием кода, ничто не мешает вам заглянуть в решение! Константаиз EggCare определяет, с какой скоро- стью яйца превращаются в свободных рабочих. Большоеколичество ра- бочихможет быть полез- но дляулья, но они также потребляют больше меда. Проблема в том, чтобы выдержать правильный баланс для рабочихраз- ных типов. Код программной части обнов- ляет элемент TextBox с от- четом о текущем состоянии в конструкторе, чтобы в про- грамме всегда выводился самый актуальный отчет. Кнопка «Assign Job» передает текст от выбранного элемента ComboBox мето- ду Queen.AssignBee, поэтому очень важно, чтобы варианты из команды switch точно соответствовали элементам ComboBox. Решение длинных упражнений
дальше 4 367 н аслед ов ание Погодите-ка ... Но разве это серьезное бизнес-приложение? Это же игра! Ладно, вы нас подловили. Да, признаем. Это игра. А есликонкретно, это игра по управлению ресурсами, т. е. игра, в которой механика сосредоточена на сборе, контроле и использованииресурсов. Каж- дый, кто играл в симуляторы вроде SimCity или в стратегические игры вроде Civilization, хорошо понимает, что управление ресурсами является важной частью игры: игроку необходимы ресурсы (деньги, металл, топливо, дерево, вода ит.д .) для функционированиягорода илипостроения империи. Игры по управлению ресурсамиотличноподходятдля экспериментов с отно- шениями между механиками, динамикой и эстетикой: Ì Механика проста: игрокназначаетрабочихизапускаетследующую смену. Затемкаждая пчела добавляетнектар,уменьшает количество нектара/ увеличивает количество меда или уменьшает количество яиц/увеличива- етколичество рабочих. Счетчикяиц увеличивается,ивыводитсяотчет. Ì С эстетикой дело обстоит сложнее. Игроки испытывают стресс, когда уровнинектара илимеда опускаются нижеустановленныхпороговина экране появляетсяпредупреждение о возможнойнехватке. Онииспыты- вают азарт,принимаярешения,иудовлетворениеотсвоего влиянияна игру, апотомснова стресс, когдапоказатели перестаютрасти и начинают снова уменьшаться. Ì Игройуправляет ее динамика. Вкоде нет ничего,что моглобыпривести кнехваткенектара или меда, — эти ресурсытолькопотребляютсяпчелами. Обманщики. Небольшое изменение в HoneyVault.NECTAR_CONVERSION _ RATIO может существенно упростить или усложнить игру из- за ускорения или замедления потребления меда. Какие еще числовые характеристики влияют на игровой процесс? Как вы думаете, что определяет эти отношения? Мозговой штурм Не пожалейте вре- мени и задумайтесь над этим, потому что здесь заложена суть игровой дина- мики. А вы найдете какие-нибудь возмож- ности для применения этих идей в других программах (помимо игр)?
368 глава 6 динамика и циклы обратной связи Серия циклов обратной связи управля- ет динамикой игры. Код, построенный вами, не будет влиять на эти циклы напрямую. Они формируются теми ме- ханиками, которые вы построите. Яйц а Мед Ра боч ие Матка о т к л а д ы в а е т Нектар превращаются п о т р е б л я е т с я с о б и р а ю т превращается п о т р е б л я е т с я потребляется п р о и з в о д я т ухаживают И эта концепция в действитель- ности очень важна во многих реальных бизнес-приложениях, не только в играх. Все, что вы здесь узнаете, вы сможете использо- вать в своей повседневной работе профессионального разработчика программного обеспечения. Обратная связь направляет работу системы управления ульем Выделим несколько минут на то, чтобы действительно разобраться, как работает игра. Коэффициент преобразованиянектара серьезно влияет на игру.Если вы из- менитеконстанты, этоможет привестикбольшимизменениямвигровомпроцессе. Еслидляпреобразованияяйца врабочегобудет достаточнонебольшойпорциимеда, игра становится слишкомпростой.Еслимеда будетнужно много,игра значительно усложняется. Но при этом никакой настройки сложности в интерфейсе игры нет. Пчелинаяматканеполучаетспециальныхулучшений,которыеупрощают игру,или же опасных врагов либо сражений сбоссами, усложняющих ее. Другимисловами, в игренет кода, явно формирующего связь между количеством яиц илирабочих исложностью игры. Что жездесь происходит? Вероятно, вам уже доводилось сталкиватьсяс обратнойсвязью.Запустите видеозвонокмеждусвоимтелефономи компьютером. Поднеситетелефон ккомпьютерномудинамику,ивы услышитегромкиеэхо-шумы. Наведите камеруна экран компьютера, и вы увидите изображениеэкрана внутри изображения экрана внутри изображения экрана, а если повернуть теле- фон, то появитсясюрреалистическийузор.Передвамиявление обратной связи: выберете живой видео-или аудиовыводи подаете егообратнопрямо навход.В коде приложения видеозвонков нет ничего,что бы конкретно генерировало эти странныеизображенияилизвуки. Ониформируются врезультатеобратнойсвязи. Рабочие и мед образуют цикл обратной связи Ваша игра поуправлению ульем основана на последовательности циклов обратной связи: множестве мелких циклов, вкоторых части игры взаимодействуют другс другом. Например,производителимеда добавляютмедв улей, гдеонпотребляетсяпроизводителямимеда,делающими ещебольшемеда. Мед Ра боч ие производят потребляется Иэто всего лишь один из циклов обратной связи. В игре присутствует много таких циклов, которые делают игру более сложной, более интересной и (хочется надеяться!) более занимательной. Цикл обратной связи между рабочи- ми и медом — лишь одна крошечная часть всей системы, управляющей игрой. Удастся ли вам обнаружить ее на большей диаграмме внизу? Когда вы направ- ляете камеру на экран, на кото - ром выводится ее видеоизобра- жение, вы тем самым создаете цикл обратной связи, порождаю- щий эти стран- ные узоры.
дальше 4 369 н аслед ов ание Циклыобратной связи... Баланс... Неявное выполнение каких-то операций за счет создания системы... У вас голова еще кру- гом не идет? Что ж, вам предоставляется еще одна возможность применить проектирование игр дляисследования более мас- штабной концепции из области программирования. Ранеевыузнали о механике, динамике и эстетике — пришло время свести эти концепции воедино. ФреймворкMDA(Mechanics- Dynamics-Aesthetics) —формальный инструмент (здесь термин «формальный» означает «сформулированный в письменном виде»), который используется теоретиками и академикамидля анализа и понимания игр. Он определяет отношения между ме- ханиками, динамикой и эстетикой идает нам возможность обсуждать, как они формируют циклы обратной связидля взаимного влияния друг на друга. ФреймворкMDAбылразработанРобиномХанике(RobinHunicke),МаркомЛебланом(Marc LeBlanc) иРобертомЗубеком(Robert Zubek), а его описание было опубликовано в статье «MDA: A Formal Approach to Game Design and Game Research» (2004 г.); статья вполне доступная и не содержит заумного академического жаргона. (Помните, как в главе 5 мы обсуждали, что эстетика включает испытания, повествование , тактильные ощущения, элементы фантастики и самовыражения? Все это взято из той статьи.) Непожалейте времени и ознакомьтесь, это очень интересно: http://bit.ly/mda-paper. Фреймворк MDA создавался для того, чтобы предоставить нам формальный механизмдля рассмотрения и анализа видеоигр.Мо- жет показаться, что этот инструмент играет важную роль только в академической среде — например, институтском учебном курсе по проектированию игр.Вдействительностифреймворк MDA весьма полезен идлярядового разработчика игр, потому что он по- могает нам лучше понять создаваемые игры идает более глубокое представление о том, что же делает эти игры интересными. Конечно, разработчики игр уже использовали термины «механика», «динамика» и «эстетика» на неформальном уровне, но в статье дается четкое определение, а также устанавливаются связи между ними. Правила Механика Система Динамика Удовольствие! Эстетика Вчастности, фреймворк MDA помогает понять, чем различаются взгляды на игруу игроков и проектировщиков игр.Игрок пре- жде всего хочет, чтобы играбыла интересной, — н о вы уже знаете, что представления об «интересном»у разных игроков могут очень сильно различаться. С другой стороны, разработчики обычно рассматривают игру с позиций механики, потому что они тратят время на написание кода, проектирование уровней, создание графики и настройку механических аспектов игры. Все разработчики (не только разработчики игр!) могут воспользоваться фреймворком MDA для более глубокого по- нимания циклов обратной связи Воспользуемсяфреймворком MDAдляанализаклассической игры Space Invaders, чтобы лучше понять циклы обратной связи. • Начнем с механики игры: корабль игрока двигается влево-вправо и стреляет снизу вверх. Пришельцы двигаются строем и стреляют сверху вниз; энергетическое поле блокирует выстрелы. Чем меньше врагов остается на экране, тем быстрее они двигаются. • Игроки открываютразличные стратегии: они стреляют супреждением, отстреливают врагов нафлангах вражеского построения, укрываются за защитным полем. В коде игры нет команд if/else или switchдлятаких стратегий;ониоткрываются по мере того, как игрок начинает глубже понимать игру.Игроки изучают правила и получаютболее глубокое представление о системе, что по - могает им более эффективно использовать правила. Другими словами, механики и динамики формируют цикл обратной связи. • Движениепришельцев ускоряется, темп звукового сопровождениярастет, игрок испытывает прилив адреналина. Игра ста- новится более интересной, и в свою очередь, игроку приходится быстрее приниматьрешения, он совершает ошибки и ме- няет стратегии, что оказывает влияние на систему.Динамика и эстетикаформируют другойцикл обратной связи. • Ничто из этого не происходило по случайности. Скорость движения пришельцев, темп, звуки, графика... Все эти факторы были тщательно сбалансированы создателем игры Томохиро Нисикадо, который провел больше года за ее разработкой, черпая вдохновение изболее ранних игр, из произведений ГербертаУэллса идаже своих собственных сновдля создания классической игры. Механика, эстетика и динамика под увеличительным стеклом
370 глава 6 пошаговое выполнение и реальное время Система управления ульем работает в пошаговом режиме... Преобразуем ее для работы в реальном времени Впошаговыхиграхигровойпроцесс делитсяначасти, вслучае с системойуправленияульем —на смены. Следующаясмена начнется только после того, каквы нажметекнопку,такчто временидляназначения рабочиху васбудетболеечем достаточно. Мы можем воспользоваться таймеромDispatcherTimer (ана- логичнымтому,которыйиспользовалсявглаве1)иперевестиигру вре- жим реального времени,вкотором игровойпроцесс идетнепрерывно, причем дляэтого потребуется лишь несколько строк кода. Добавьте команду using в начало файла MainWindow.xaml.cs. Мы будем использовать таймер DispatcherTimer для принудитель- нойотработкиследующейсменычерез каждые1.5 секунды. Класс DispatcherTimer принадлежит пространству именSystem.Windows. Threading, поэтому в началоMainWindow.xaml.cs необходимо до- бавить следующую строку using: using System.Windows.Threading; Добавьте приватное поле, содержащее ссылку на DispatcherTimer. Теперь нужносоздать новыйэкземплярDispatcherTimer. Сохраните его вприватном поле вначале класса MainWindow: private DispatcherTimer timer = new DispatcherTimer(); Заставьте таймер вызывать метод-обработчик события Click кнопки WorkShift. Таймер должен постоянно двигать игрувперед, так что если игрок не щелкнетна кнопке доста- точно быстро, новая смена запускается автоматически. Начнитес добавления следующего кода: public MainWindow() { InitializeComponent(); statusReport.Text = queen.StatusReport; timer.Tick += Timer_Tick; timer.Interval = TimeSpan.FromSeconds(1.5); timer.Start(); } private void Timer_Tick(object sender, EventArgs e) { WorkShift_Click(this, new RoutedEventArgs()); } А теперь запустите игру.Новая смена начинается каждые 1.5 секунды независимо оттого, нажали вы кнопкуили нет. Этонеособозначительноеизменениевмеханикерадикально меняет динамику игры, что приводитк колоссальным изменениям в эстетике. А уж какиграработаетлучше— впошаговом ре- жимеили врежимереального времени— решать вам. 1 2 3 К ласс Di spat ch erT imer использовался в главе 1 для включения таймера в игру с поиском пар. Этот код очень похож на тот, который использовался в главе 1. Потратьте не- сколько минут и вернитесь к тому проекту, чтобы припомнить, как работает D isp at cher Ti mer. Как только вы введете +=, Visual Studio предло- жит создать обработ- чик события Timer_Tick. Нажмите Tab, чтобы IDE сгенерировала ме- тод за вас. Tаймер вызывает обработчик события Tick каждые 1.5 се- кунды, который, в свою очередь, вызывает обработчик события кнопки WorkShift.
дальше 4 371 н аслед ов ание Для добавления таймера потребовалось лишь несколько строк кода, но от этого игра полностью изменилась. Не потому ли это произошло, что введение таймера сильно повлияло на связи между механиками, динамикой и эстетикой? Да! Таймер изменил механику, это привело к изменению динамики, а та, в свою очередь, повлияла на эстетику. Задержитесь на минуту и обдумайте этот цикл обратной связи. Изменениемеханики(таймер, который автоматическинажимает кнопку «Work theNext Shift» через каждые 1.5 секунды)создает совершенно новую динамику: временноеокно,за котороеигрок должен принять решение, иначе игра приметрешение за него. Игра становитсяболее напряженной, отчего некоторые игро- ки получают дозу адреналина,но у других это только вызывает стресс— эстетика изменилась. Игра становитсяболееинтересной для одних людей, но менее интересной для других. Приэтомвыдобавили вигрувсегополдюжины строккода,в ко- торых небыло логики «примирешение, иначе». Это еще один пример поведения, проявляющегося в результате совместной работы таймера и кнопки. Похоже, все эти обсуждения циклов обратной связи важны, особенно та часть, где речь идет о проявлении поведения. Циклы обратной связи и проявление поведения — ва жные концеп ции програ мм ирова ния . Мыразрабатывалиэтот проект для того, чтобывы моглипотре- нироватьсяснаследованием, атакже длятого,чтобыпредоста- вить вамвозможность поэкспериментировать с проявляемым поведением. Речь идет о поведении, которое обусловлено не только тем, что делают ваши объекты сами по себе, но и тем, как объектывзаимодействуютдругсдругом.Игровыеконстанты (такие, как коэффициент преобразования нектара) являются важнойчастьютакихпроявляемыхвзаимодействий.Создавая этоупражнение, мысначалаинициализировалиэтиконстанты некоторымизначениями,а затем сталивноситьнебольшиеиз- менения, пока не получилась система, которая не находилась в равновесии(состояние,в котором все идеально сбалансиро- вано), так что игрок вынужден принимать решения для того, чтобыиграпродолжалась максимальновозможное время. Ивсе это обусловленоцикламиобратнойсвязи между яйцами,рабо- чими,нектаром,медом и пчелинойматкой. Попробуйте поэкс- периментировать с этими циклами обратной связи. На- пример, увеличьте количество яиц на смену или исходный запас медавулье — и игра станет про- ще. Не стесняйтесь, пробуйте! Незначи- тельные изменения всего нескольких конс тан т мо гут в корне изменить восприятие игры. И здесь тоже присутствует цикл обратной связи. Игроки, испытывающие стресс, обычно принимают ме- нее эффективные решения, изменяя игру... Эстетика становится ис- точником об- ратной связи для механики.
372 глава 6 во зв ращ аемся к н аслед ов анию Экземпляры некоторых классов никогда не должны создаваться Помните нашу иерархию классов изсимулятора зоопарка?В любом нормальном зоопарке вы создадите экземплярыконкретныхобитателей —классовHippo,DogилиLion...АкакнасчетклассовCanine и Feline? КакнасчетклассаAnimal? Оказывается, существуют классы, экземплярыкоторыхвообще никогда не долж- ны создаватьсявпрограмме... Идаже еслибыонибыли созданы, то никакого смысла в нихне былобы! Звучит странно?Вообще-товстречаетсясплошь ирядом— собственно, ранее вэтойглаве выуже создали несколько классов,которые никогда недолжны воплощаться ввиде конкретных экземпляров. Ost ri ch pr otected ov err ide Lay E ggs Pig eon pr otec ted over ri de LayE ggs Bird stati c Randomi zer protec ted v ir tual Lay Eggs Swo rd Damag e protec ted over ri de Calc ul ateDamage Ar ro wDamag e protec ted ove rri de Cal cul ateDamage We aponD am age Rol l Magi c Fl ami ng Damage pr otected v ir tual Cal c ulateDam age class Bird { public static Random Randomizer = new Random(); public virtual Egg[] LayEggs(int numberOfEggs) { Console.Error.WriteLine ("Bird.LayEggs should never get called"); return new Egg[0]; } } class WeaponDamage { /* ... код свойств ... */ } protected virtual void CalculateDamage() { /* the subclass overrides this */ } public WeaponDamage(int startingRoll) { roll = startingRoll; CalculateDamage(); } } КлассBird был совсем крошечным — он содержал только общий экземпляр Random и метод LayEggs, который существовал только для его переопре- дел ени я в субк л ассах. Кл асс Weapo nD amage был намного больше — он содержал целый набор свойств. Он также содержал метод CalculateDamage, предназначенный для переопре- деления субклассами, который вызывался из его мето д а Weap on Damag e.
дальше 4 373 н аслед ов ание NectarCollector overridefloatCostPerShift pr otec ted over ri de DoJob HoneyManufacturer overridefloatCostPerShift protec ted over ri de DoJ ob EggC ar e overridefloatCostPerShift protec ted ov erri de DoJ ob Queen s tri ng S tatusRep ort (read-only) overridefloatCostPerShift private Bee[] workers AssignBee Car eFor E ggs pr otected ov err ide D oJob Bee stringJob virtualfloatCostPerShift ( read- onl y) W ork TheNex tS hift pro tected v ir tual D oJob class Bee { public virtual float CostPerShift { get; } public string Job { get; private set; } public Bee(string job) { Job = job; } public void WorkTheNextShift() { if (HoneyVault.ConsumeHoney(CostPerShift)) { DoJob(); } } protected virtual void DoJob() { /* переопределяется субклассом */ } } Что же произойдет при попытке создания экземпляра класса Bird, WeaponDamage или Bee? Имеет ли это смысл хоть когда-нибудь? Будут ли работать методы таких классов? Мозговой штурм КлассBee содержит метод WorkTheNextShift, кото- рый потреблял мед и после этого выполнял задание, назначенное пчеле, — п оэ то му предполагалось, что субкласс переопределяет метод DoJob, в котором не- посредственно выполнялась работа. Экземпляры классаBee не созда- ются ни в одной точке кода систе- мы управленияульем. Непонятно, что должно происходить при попытке создания такого экзем- пляра, потому что для него нигде не зад ается сто им ост ь отр або тки смен ы.
374 глава 6 создать экземпляр абстрактного класса невозможно Абстрактный класс ¦ намеренно незавершенный класс Достаточночастовпрограмме создаются классы с методами-«заполнителями», которые должны реа- лизоватьсяв субклассах. Такойкласс можетнаходитьсяна вершинеиерархии(какBee,WeaponDamage или Bird)илижевеесередине (какFeline и Canineв модели классов симулятора зоопарка).Ониполь- зуются темфактом, что C# всегда вызывает самую конкретнуюреализацию метода — подобно тому, как WeaponDamage вызывает метод CalculateDamage, который реализован только в SwordDamage или ArrowDamage, или как Bee.WorkTheNextShift зависит от реализации метода DoJob субклассами. В C#для этой цели существует специальный инструмент: абстрактный класс. Это класс, который на- мереннооставленнезавершенным; онсодержит пустыекомпоненты,которыеслужат заполнителямии должныбыть реализованы всубклассах. Чтобы объявить класс абстрактным,добавьтеключевое слово abstract вобъявление класса. Об абстрактныхклассах необходимо знать следующее: Абстрактный класс работает так же, как и обычный. Абстрактныйкласс определяется точно так же,как и обычный.Онсодержит поля и методы,он можетнаследовать отдругихклассовит. д. Абстрактный класс может иметь незавершенные компоненты-«заполнители». Абстрактный классможетвключать объявлениясвойстви методов, которые должныбытьреализова- нынаследующимиклассами.Метод,укоторогоесть объявление,нонет тела,называетсяабстрактным методом. Свойство, котороетолько объявляет свои методыдоступа,ноне определяет их,называется абстрактным свойством. Субклассы, расширяющие абстрактный класс, обязаныреализовать все абстрактные методыи свойства; впротивномслучае они также останутся абстрактными. Только абстрактные классы могут иметь абстрактные методы и свойства. Есливывключаетеабстрактныйметодили свойство в класс, вы должны пометить этоткласс как абстрактный;впротивном случаекодкомпилироватьсянебудет. (Вскоре выузнаете, какпометить класс какабстрактный.) Экземпляры абстрактных классов создаваться не могут. Конкретное — противоположность абстрактному.Конкретный метод имеет тело,и все классы, с которымивыработалидо сих пор,были конкретными. Главноеразличие между абстрактным иконкретным классомзаключается втом, чтоэкземпляр абстрактногоклассаневозможносоздать командойnew. Есливыпопытаетесь это сделать,C# выдаст ошибку прикомпиляциикода. Попробуйте иубедитесь! Создайте новое консольное приложение, добавьте в него пустой аб- страктныйкласс ипопробуйтесоздать экземпляр: abstract class MyAbstractClass { } class Program { MyAbstractClass myInstance = new MyAbstractClass(); } Компилятор выдастсообщениеоб ошибке иоткажется строить ваш код:     Компилятор не позволяет создать экземпляр аб- страктного класса, по- тому что абстрактные классы не предназначены для создания экземпляров.
дальше 4 375 н аслед ов ание Например, если вы хотите предоставить часть кода, но при этом требуете, чтобы остальной код был обязательно определен субклассами. Иногда присозданииобъектов,которыесоздаватьсянедолжны,впрограммемогут проис- ходить разныенеприятности.Классы, находящиесяна вершине диаграммыклассов,обычно содержат поля, которые должны инициализироваться субклассами. Класс Animal может выполнить вычисления, которыезависят от логического значенияHasTail илиVertebrate, но приэтому него нетвозможностизадать это значениесвоимисилами.Короткий пример класса, созданиеэкземпляра которого создает проблемы... Погодите, что? Класс, экземпляр которого я даже не могу создать? Для чего вообще определять такие классы? class PlanetMission { protected float fuelPerKm; protected long kmPerHour; protected long kmToPlanet; public string MissionInfo() { long fuel = (long)(kmToPlanet * fuelPerKm); long time = kmToPlanet / kmPerHour; return $"We'll burn {fuel} units of fuel in {time} hours"; } } class Mars : PlanetMission { public Mars() { kmToPlanet = 92000000; fuelPerKm = 1.73f; kmPerHour = 37000; } } class Venus : PlanetMission { public Venus() { kmToPlanet = 41000000; fuelPerKm = 2.11f; kmPerHour = 29500; } } class Program { public static void Main(string[] args) { Console.WriteLine(new Venus().MissionInfo()); Console.WriteLine(new Mars().MissionInfo()); Console.WriteLine(new PlanetMission().MissionInfo()); } } А вы сможете предсказать, что будет выведено на консоль, до запуска кода? Сделайте э то!
376 глава 6 абстрактные классы помогают избежать выдачи исключения Как мы уже говорили, экземпляры некоторых классов не должны создаваться ни при каких условиях Попробуйтезапустить консольноеприложениеPlanetMission. Произошлолито,что вы предполагали? На консоль выводятся две строки: We'll burn 86509992 units of fuel in 1389 hours We'll burn 159160000 units of fuel in 2486 hours Апотомпроисходитисключение. Все проблемы начались с создания экземпляра класса PlanetMission. Его метод FuelNeeded ожидает, что значения полей будут заданы субклассом. Если этого не происходит, поля сохраняютзначение по умолчанию — нуль. Икогда C#пытается разделить число на нуль... Решение: используйте абстрактный класс Если класспомечен ключевымсловомabstract, C# не позволит вам написать кодсоздания его экземпляра.Какже эторешает проблему?По старойпо- говорке: профилактика лучшелечения.Добавьтеключевоеслово abstract в объявление класса PlanetMission: abstract class PlanetMission { // Остальной код класса остается без изменений } Как только вы внесете это изменение, компилятор сообщит об ошибке: Код вообще не компилируется, а если нетоткомпилированного кода, то нети исключения. Ситуация напоминаетиспользованиеключевого слова private вглаве 5илиже ключевых словvirtual и override ранеевэтойглаве. Объявление компонентов класса приватными неизменяет его поведение,оновсего лишь не позволит вашему коду построиться при нарушенииинкапсуляции. Ключевое слово abstract работает аналогичным образом: вы неполучите исключение при создании экземпляра абстрактного класса,потому что компиляторC# не позволит вам создать этот экземпляр. Если добавить клю- чевое слово abstract в объявление класса, то компилятор будет выдавать ошибку при любых попытках создания экземпляра этого класса.
дальше 4 377 н аслед ов ание У абстрактных методов нет тела У классаBird, который вы построилиранее,экземпляры создаваться не должны. Именно поэтому он используетConsole.Errorдлявыводасообщенияобошибке,еслипрограммапопытаетсясоздать экземп- ляр и вызвать метод LayEggs: class Bird { public static Random Randomizer = new Random(); public virtual Egg[] LayEggs(int numberOfEggs) { Console.Error.WriteLine ("Bird.LayEggs should never get called"); return new Egg[0]; } } Таккакмыхотимпредотвратить созданиеэкземпляровклассаBird,добавимключевое слово abstract в его объявление.Тем неменееэтогонедостаточно — кромезапрета на создание экземпляров, мы также хотим потребовать, чтобы каждый субкласс, расширяющийBird,обязательно переопределял метод LayEggs. Именноэтопроисходитпридобавленииключевогослова abstractк компонентукласса. Абстрактныйметод существует только ввиде объявления, ноунего неттеламетода, которое должно бытьреализовано каждымсубклассом, расширяющим абстрактный класс. Тело метода состоит из кода, заключенного в фигурныескобки иследующего послеобъявления, — и это то, чего уабстрактныхметодов небываетпо определению. Вернитесь к проектуBird изаменитеклассBirdследующим абстрактным классом: abstract class Bird { public static Random Randomizer = new Random(); public abstract Egg[] LayEggs(int numberOfEggs); } Ваша программа работаетточно так же, как прежде! Но попробуйте включить следующую строку в метод Main: Bird abstractBird = new Bird(); Компилятор выдает сообщение об ошибке: Попробуйте добавить тело к методу LayEggs: public abstract Egg[] LayEggs(int numberOfEggs) { return new Egg[0]; } Выснова получитеошибкукомпиляции,только другую: Жизнь абстрактного методаужасна. Ведь это жизнь без тела. Если абстракт- ный класс содержит виртуальные компоненты, то каждый суб- класс должен пе рео преде лять все такие ком- поненты.
378 глава 6 свойства могутбыть абстрактными Абстрактные свойства работают как абстрактные методы ВернемсякклассуBee изпредыдущего примера.Мыуже знаем, что экземплярыэтого класса создаваться недолжны, поэтомупреобразуемего вабстрактныйкласс.Дляэтогодостаточнодобавить модификатор abstract вобъявление класса ипреобразовать DoJob в абстрактный методбез тела: abstract class Bee { /* Остальной код класса остается без изменений */ protected abstract void DoJob(); } Однакосуществует еще один виртуальный компонент— иэтонеметод.МыговоримосвойствеCostPerShift, которое вызывается методом Bee.WorkTheNextShift для определения того, сколько меда потребуется пчелена эту смену: public virtual float CostPerShift { get; } Вглаве5выузнали,чтосвойства вдействительностипредставляютсобойобычныеметоды,ккоторым вы обращаетесь как к полям. Для создания абстрактного свойства используется ключевое слово abstract,как ивслучае с методом: public abstract float CostPerShift { get; } Абстрактные свойства могутиметь get-метод и/илиset-метод доступа. Set- и get-методы в абстрактных свойствах не могут иметь тела. Их объявления выглядят как объявления автоматических свойств, но таковыми не являются, потому что не содержат никакойреализации. Абстрактные свойства, как и аб- страктные методы, представляют собой«заполнители» длясвойств, которые должныбытьреализованы любым субклассом, расширяющим свойкласс. Нижеприведенполный кодабстрактного классаBee,вместес абстрактным методом исвойством: abstract class Bee { public abstract float CostPerShift { get; } public string Job { get; private set; } public Bee(string job) { Job = job; } public void WorkTheNextShift() { if (HoneyVault.ConsumeHoney(CostPerShift)) { DoJob(); } } protected abstract void DoJob(); } ЗаменитеклассBee всистемеуправленияульем новым абстрактным классом. Приложениевсеравно будет работать!А теперь попытайтесь создать экземпляр класса Bee командой new Bee(); компилятор выдаст сообщение об ошибке.Но что ещеважнее, есливыпопытаетесь расширитьBee, но забудетереали- зовать CostPerShift, произойдет ошибка. Замените!
дальше 4 379 н аслед ов ание Пришло время потренироваться в использовании абстрактных классов. К счастью, искать кан - дидатов для преобразования в абстрактные классы долго не придется. Ранее в этой главе мы изменили классы SwordDamage и ArrowDamage так, чтобы они расширяли новый класс с именем WeaponDamage. Преобразуйте класс WeaponDamage в абстрактный. В классе WeaponDamage также имеется хороший кандидат для преобразования в абстрактный метод — преобразуйте его. В: Когда я помечаю класс как абстрактный, как это влияет на его поведение? Методы или свойства абстрактного класса чем-то отличаются от методов или свойств конкретного класса? О: Нет, абстрактные классы работают точно так же, как любые другие классы. Когда вы добавляете ключевое слово abstract в объявление класса, компилятор C#делаетдве вещи:(1)онзапре- щает использовать класс в командах new, (2) позволяет включать в класс абстрактные методы и свойства. В:Некоторые абстрактные классы, которые вы показывали, были открытыми; другие были защищенными (protected). На что это влияет? И важен ли порядок этих ключевых слов в объявлении класса? О: Абстрактные методы могут иметь любой модификатордосту- па. Если вы объявите абстрактный метод приватным, то классы , реализующие этот абстрактныйметод,также должны объявить его приватным. Порядок ключевых слов нина чтоневлияет.Объявления protected abstractvoidDoJob();иabstractprotected void DoJob();делают абсолютно одно и то же. В: Меня смущает то, как вы используете термины «реа- лизовать» или «реализация». Что вы имеете в виду, говоря о реализации абстрактного метода? О: Когда вы используете ключевое слово abstractдля объ- явления абстрактного метода или свойства, вы фактически опре- деляетеабстрактный компонент класса. Позднее придобавлении в конкретный класс завершенного метода или свойства с таким же объявлением вы реализуете этот компонент. Итак, вы опре- деляете абстрактные методы или свойства в абстрактном классе и реализуете их в конкретных классах, которые расширяют этот абстрактный класс. В: Я так и непонял, в чем смысл того , что ключевое слово abstract не позволяет моему кодукомпилироваться, если я пы - таюсь создать экземпляр абстрактного класса.Я ужепотратил достаточно времени на поиск и исправление всех ошибок компиляции. Зачем же мнеусложнять построение своего кода? О:Когдавытолькоизучаетепрограммирование,ошибкикомпилято- ра«CS»становятся досаднойпомехой.Всемнамдоводилось тратить время на поиски пропущенной запятой, точки или вопросительного знака, чтобы очистить список Error List. Тогда зачем использовать ключевые слова вроде abstract или private, которые только создают больше ограниченийдля вашего кода и повышают вероят- ность ошибок компиляции?Напервый взгляд это выглядит противо- естественно. Если не использовать ключевое слово abstract, то вы никогда не получите ошибку компилятора «Невозможно создать экземпляр абстрактного класса». Тогда зачем его использовать? Причина, по которой мы используем такие ключевые слова, как abstract и private, препятствующие построению вашего кода в некоторых случаях, проста. Исправить ошибку компилятора «Не- возможно создать экземпляр абстрактного класса» гораздо проще, чем ту ошибку, которую это сообщение предотвращает. Если у вас имеется класс, экземпляры которогоникогданедолжны создаваться в программе, случайное создание экземпляра этого класса вместо одного из субклассов может стать исключительно коварной и труд- ноуловимой ошибкой.Добавление ключевого слова abstract в базовый класс ускоряет проявление сбоя с ошибкой, которая проще исправляется. Ошибки, возникающие из-за создания экземп- ляра базового класса, который создаваться никогда не должен, бывают особенно коварны- ми и нетривиальными. Объявление класса аб- страктным ускоряет проявление сбоя в вашем коде, если вы попытаетесь создать экземпляр этого класса. Упражнение часто Зада вае мые вопросы
380 глава 6 это защита, а не ограничение Экземпляры класса WeaponDamage создаваться не должны — этот класс существует только для того, чтобы классы SwordDamage и ArrowDamage могли наследовать его свойства и методы. А значит, будет логично объявить этот класс абстрактным. Взгля- ните на его метод CalculateDamage: protected virtual void CalculateDamage() { /* переопределяется субклассом */ } Этот метод становится отличным кандидатом для преобразования в абстрактном классе, потому что он существует только для того, чтобы субклассы могли переопре- делить его собственными реализациями, обновляющими свойство Damage. Ниже при- ведено все необходимое для внесения изменений в классWeaponDamage: abstract class WeaponDamage { /* Свойства Damage, Roll, Flaming и Magic остаются неизменными */ protected abstract void CalculateDamage(); public WeaponDamage(int startingRoll) { roll = startingRoll; CalculateDamage(); } } Это был первый раз, когда вы перечитывали код, написанный для предыдущих упражнений? Возвращениекнаписанномуранеекодуможет восприниматьсянемногостранно. Нонасамом деле многие разработчикитак поступают,и это полезнаяпривыч- ка. Вы находили в кодечто-то такое, что со второго захода сделали бы иначе? Обнаруживали какие-то улучшения или изменения, которые стоило бы внести вкод? Никогда не жалейтевремени нарефакторинг вашего кода. Именно это былосделано вданном упражнении:мыизменилиструктурукода безизменения его поведения. Это и называетсярефакторингом. Спасибо за рефакторинг! Уверен, вы предотвратили немало противных ошибок в будущем. Теперь я могу больше думать о своей игре, а не о коде. Отличная работа! Упражнение Решение
дальше 4 381 н аслед ов ание Наследование — очень полезная штука. Я могу определить метод в базовом классе, и он автоматически появится в каждом из субклассов. А если я захочу сделать это с двумя методами в двухразных классах? Может ли один субкласс расширять два базовых класса? Звучит заманчиво! Но есть одна проблема. ЕслибыC#допускалнаследование отнесколькихбазовыхклассов, впро- граммах открылась бы целая куча неприятностей.Если язык позволяет одному субклассу наследовать от двухбазовых классов,это называется множественным наследованием. А еслибы вC#поддерживалось мно- жественноенаследование,то этонеизбежно привелобыкколоссальной и почтинеразрешимой проблеме,которая называется... Смертельный ромб Oven Temperatur e ov err ide Tur nOn To ast er SlicesOfBread over ri de TurnO n Appl ia nce abs trac t Tur nOn ToasterOven Temper ature SlicesOfBread Which TurnOn methoddoes To aster O ven in her it? Что происходило бы в БЕЗУМНОМ мире, в котором в C# поддерживалось бы мно- жест венн ое насл ед ован ие? Дав айт е сы- граем в «Что, если...» и выясним это. Что, если... у вас имеется класс с именем App- liance, который содержит абстрактный метод с им енем Tu rnOn? И что, если... он имеет два субкласса:Oven со свойствомTemperatureи Toaster со свойством SlicesOfBread? И что, если... вы хотите создать класс Toas- terOven, наследующий как Temperature, так и SlicesOfBread? И что, если... в C# поддерживалосьбы множе- ственное наследование и такое было бы воз- можно? Остается последний вопрос... Какую версию TurnOnунаследует ToasterOven? Версию изOven?Иливерсию изToaster? Это невозможно определить заранее! И именно поэтому множественное наследова- ние вC# неподдерживается. Печь (Oven ) и Тостер (Toaster) наследуют от класса Электроприбор (Appliance) и переопре- деляют метод TurnOn. Если вам понадобится класс ToasterOven, было бы очень удобно, если бы мы могли унасле- довать Temperature от Oven и SlicesOfBread от Toas te r. Метод TurnOn пере- определяется обо- ими классами, Oven и Toaster. Если бы C# позволил расширить Oven одновремен- но с Toaster, какую версию TurnOn должен был бы получить класс ToasterOven? Это настоящее название! Не- которые разработчики так- же называют ее «проблемой ромбовидного наследования».
382 глава 6 мечты, мечты ¢ Субкласс может переопределять унаследованные ком- поненты, заменяя их новыми методами или свойствами с такими же именами. ¢ Чтобы переопределить метод или свойство, добавьте ключевое слово virtual в базовый класс, а затем добавьте ключевое слово override в одноименный метод или свойство в субклассе. ¢ Ключевое слово protected — модификатор досту- па, с которым компонент класса является открытым только для своих субклассов, но остается приватным для всехостальныхклассов. ¢ Когда субкласс переопределяет метод из своего базо- вого класса, то всегда вызывается самая конкретная версия, определенная в субклассе, даже если она вы- зывается базовым классом. ¢ Если субкласс просто добавляет метод с таким же име- нем, как у метода базового класса, он просто скрывает метод базового класса вместо того, чтобы переопреде- лить его. Используйте ключевое слово newдля сокры- тия методов. ¢ Динамика игры описывает то, как механики комбиниру- ются и взаимодействуют друг с другом для управления игровым процессом. ¢ Субкласс может обратиться к своему базовому классу при помощи ключевого слова base. ¢ Субкласс и базовый класс могут иметь разные кон- структоры. Субкласс может выбрать, какие значения передать конструкторубазового класса. ¢ Постройте модель классов на бумаге, прежде чем писать код. Это поможет вам лучше понять суть проб- лемы. ¢ Если перекрытие между классами минимально, это яв- ляется проявлением важного принципа проектирования, называемого разделением обязанностей. ¢ Проявляемое поведение раскрывается при взаимо- действии объектов друг с другом, за границами логики, напрямую запрограммированной в них. ¢ Абстрактные классы представляют собой намеренно незавершенные классы, экземпляры которых не могут создаваться в программе. ¢ Если выдобавите ключевое слово abstract в метод или свойство и исключите его тело, метод или свойство становятся абстрактными. Любой конкретный субкласс абстрактного класса должен реализовать этот метод или свойство. ¢ В процессе рефакторинга разработчик читает ранее написанный код и вносит в него улучшения, не изменяя его поведения. ¢ В C# не поддерживается множественное наследование из-за проблемы ромбовидного наследования: C# не может определить, какую версию компонента, унаследо- ванного от двухбазовых классов, следуетиспользовать. КЛЮЧЕВЫЕ МОМЕНТЫ Но, наверное, об этом можно только мечтать... А хорошо бы, чтобы в C# было что-то вроде абстрактного класса, но только без проблемы ромбовидного наследования, чтобы C# позволял расширить сразу несколько таких псевдоклассов одновременно?
Лабораторный курс Unity No3 https://github.com/head-first-csharp/fourth-edition Head First C# Лабораторный курсUnityNo 3 383 C# является объектно-ориентированным языком. А по- скольку суть всех лабораторных работ Unity заключается в получении практического опыта написания кода C#, вполне разумно, что в этих лабораторных работах цен- тральное место занимает создание объектов. Вы уже создавали объекты в C# с того момента, когда узнали о существовании ключевого слова new в главе 3. В этой лабораторной работе Unity мы создадим экзем- пляры объектов Unity GameObject и воспользуемся ими в полноценной, работоспособной игре. Это станет отлич- ной отправной точкой для написания Unity-игр на C#. В следующих двух лабораторных работах Unity мы соз- дадим простую игру, в которой будет задействован уже знакомый вам бильярдный шар. В этой игре мы возьмем за основу то, что вы ранее узнали об объектах C# и эк- земплярах. Мы воспользуемся заготовками (инструментом Unity для создания экземпляров GameObject) для создания множества экземпляров GameObject, а затем используем сценарии, чтобы заставить объекты GameObject летать в тре хм ерном прост ран стве и гры. Лабораторный курс Unity No 3 Экземпляры GameObject
384 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 3 Построим игру в Unity! ПлатформаUnityпредназначенадлясозданияигр.Такимобразом, вследующихдвухлабораторныхрабо- тах Unityвыприменитето, чтознаете о C#, дляпостроенияпростойигры. Игра выглядит примернотак: Итак, за дело!Какобычно, первое, что необходимо сделать, — создать проектUnity.На этотразмыбудем хранитьфайлывчутьболее структурированномвиде, поэтомудляматериаловисценариевбудут созданы отдельныепапкии ещеодна папка — для заготовок (о них выузнаете позднеев этой главе). 1. Преждечем братьсяза дело,закройте всеоткрытые проектыUnity.Такжезакройте средуVisual Studio — Unity откроетееза вас. 2. Создайтеновый проектUnity по3D-шаблону, как это делалось в предыдущих лабораторныхра- ботахUnity.Присвойтеемуимя,котороепоможет вамзапомнить,вкаких лабораторныхработах ониспользуется(например, «UnityLabs3 and4»). 3. ВыберитемакетWide, чтобываш экрансоответствовалприводимым снимкам экрана. 4. Создайтепапку дляматериаловвнутрипапкиAssets. Щелкнитеправойкнопкой мыши на папке Assets в окнеProject, выберите команду Create>>Folder. Присвойте ей имя Materials. 5. СоздайтевAssets новую папку с именемScripts. 6. СоздайтевAssets еще одну папку с именемPrefabs. Проследите за тем, что- бы папки Materials, Scripts и Prefabs были созданы внутри папки Assets. В окне Project пустые папки отображаются в контурном виде. После завершения игры кнопка Play Again запу- скает новую игру. При запуске игры сцена медленно заполняется бильярдными шарами. Игрок должен щел- кать на шарах, чтобы они исчезали с экрана. Если в сцене накопится 15 шаров, игра завер- шается. В правом верхнем углу выводится текущий счет. За каждый шар, на котором щелкнул игрок, ему начисляется победное оч ко.
Лабораторный курс Unity No3 Head First C# Лабораторный курсUnityNo 3 385 Создайте новый материал в папке Materials Сделайте двойнойщелчокнапапке Materials, чтобыоткрыть ее.Вэтой папке мысоздадимновый материал. Перейдите по адресуhttps://gitgub.com/head-first-csharp/fourth-edition и щелкните на ссылке Billiard BallTextures(какэтобылосделановпервойлабораторнойработеUnity), чтобы загрузитьфайлтекстуры 1BallTexture.png впапкуна вашемкомпьютере.ПеретащитезагруженныйфайлвпапкуMaterials— так же, как было сделано с загруженнымфайлом в первой лабораторнойработеUnity, только на этот раз перетащите его в созданную вамипапкуMaterials(вместородительской папкиAssets). Теперьможносоздать новыйматериал. Щелкните правойкнопкоймышина папкеMaterials вокнеProject ивыберитекоманду Create>>Material.Присвойтеновомуматериалу имя1Ball.Ондолженпоявиться впапкеMaterials в окнеProject. Проследите за тем, чтобы в окнеMaterials был выбранматериал 1 Ball; он должен отображаться вокне Inspector. Щелкнитенафайле1BallTextureи перетащитеего на полеслева отметки Albedo. Крошечное изображение текстуры 1 Ball должно появиться в поле слева отпунктаAlbedo в окнеInspector. Теперь при наложении на сферу вашего материаларезультат вы- глядит какбильярдныйшар. Выберите матери- ал 1 Ball в окне Project, чтобы просмотреть его свойства. Перетащите карту текстуры на поле слева от метки Albedo. Объекты GameObject отражают свет своими поверхностями. Когда вы видите в игре Unity объект, обладающий цветом или текстурнойкартой, вына самом деле видите поверхность объекта GameObject, отражающую свет в сцене. Цветом этой поверхно- стиуправляетальбедо.Термин«альбедо» происходитизфизики (аконкретно изастрономии),ионобозначаетцвет,отражаемый объектом. Дополнительную информацию об альбедо можно най- ти вруководствеUnity.Выберитекоманду«UnityManual» вменю Help,чтобы открытьруководство в браузере,ипроведите поиск по строке «albedo» — одна из страниц руководства объясняет за- висимость цвета и прозрачностиотальбедо. За сценой В предыдущихлабораторных работах Unity мы использова- ли текстуру —растровый графический файл, в который Unity может «оборачивать» объекты GameObject. При перетаскива- нии текстуры на сферуUnityавтоматически создает материал, который используется Unity для хранения информации о том, как должен визуализироваться объект GameObject, содержащий ссылку на текстуру. На этотраз материал создается вручную. Как и в предыдущем упражнении, для загрузки PNG-файла текстуры необходимо щелкнуть на кнопке Download на странице GitHub.
386 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 3 Создание бильярдного шара в случайной точке сцены Создайте новый объектSphere при помощи сценария OneBallBehaviour: Ì Выберитекоманду3DObject>>Sphereизменю GameObject, чтобысоздать сферу. Ì Перетащите на нееновый материал1Ball,чтобы сфера выглядела как бильярдный шар. Ì Затем щелкнитеправой кнопкой мышина папкеScripts, созданной вамив окнеProject, и соз- дайте новый сценарий C# с именем OneBallBehaviour. Ì Перетащите сценарийна сферу вокнеHierarchy.Выделитесферуиубедитесь в том,что вокне Inspector отображается компонентScript с именем «One Ball Behaviour». Сделайте двойной щелчок на сценарии, чтобы отредактировать его вVisual Studio. Добавьте точно такой же код,который вы использоваливBallBehaviourиз первойлабораторнойработыUnity, а затем закомментируйтестрокуDebug.DrawRay вметодеUpdate. Ваш сценарий OneBallBehaviour должен выглядеть так: public class OneBallBehaviour : MonoBehaviour { public float XRotation = 0; public float YRotation = 1; public float ZRotation = 0; public float DegreesPerSecond = 180; // Start вызывается перед первым обновлением кадра void Start() { } // Update вызывается один раз на кадр void Update() { Vector3 axis = new Vector3(XRotation, YRotation, ZRotation); transform.RotateAround(Vector3.zero, axis, DegreesPerSecond * Time.deltaTime); // Debug.DrawRay(Vector3.zero, axis, Color.yellow); } } Теперь измените метод Start так, чтобы при создании сфера перемещалась в случайную позицию. Для этогомывоспользуемся свойством transform.position, котороеизменяетпозициюобъектаGameObject в сцене. Нижеприведенкод позиционирования шара в случайной точке— добавьте его в метод Start всценарииOneBallBehaviour: // Start вызывается перед первым обновлением кадра void Start() { transform.position = new Vector3(3 - Random.value * 6, 3 - Random.value * 6, 3 - Random.value * 6); } Запустите свою игру кнопкойPlay вUnity.Шар должен вращаться по осиY в случайной точке. Оста- новитеи запуститеигрунесколькораз. Каждыйразшар долженпоявляться вновойточкесцены. Когда вы добавляете метод Start к объ- екту GameObject, Unityбудет вызывать этот метод каждый раз, когда в сцену добавляется новый экземпляр этого объекта. Если метод Start находится в сценарии, присоединенном к объек- ту GameObject, который отображается в окне Hierarchy, этот метод будет вы- зван сразу же после запуска игры. Эта строка не понадобится, закомментируйте ее. Помните: кнопка Play не сохраняет вашу игру! Сохраняйтесь пораньше, сохраняйтесь почаще... Unity часто создает экземпляр объекта GameObject за какое-то время до того, как он будет добавлен в сцену. Метод Start объекта GameObject вызывается только при фактическом добавлении GameObject в сцену. Мы не включаем строки using в код сцена- рия, а просто считаем, что они есть.
Лабораторный курс Unity No3 Head First C# Лабораторный курсUnityNo 3 387 Применение отладчика для понимания Random.value Мыуже неоднократно использоваликлассRandom изпространства имен.NET System —вчастности, для распределенияпар животныхвигреизглавы1,а также длявыбора случайных картвглаве3.Но сейчас перед вами другой класс Random — попробуйте навестиуказатель мыши на ключевое словоRandom вVisualStudio. Оба этих класса называются Random, но если навести указатель мыши на них в Visual Studio, то вы увидите, что использовавший- ся ранее класс принадлежит пространству имен System. А сейчас мы используем класс Random из пространства имен UnityEngine. Из кода выбора случайных карт, написанного вами ранее. Изкода видно,что новый классRandom отличается от того, который использовалсяранее. Тогда для получения случайного значения вызывался метод Random.Next и полученное значениебыло целым числом. В новом коде используетсяRandom.value, но это неметод,а свойство. Воспользуйтесь отладчиком Visual Studio для просмотра видов значений, которые вам может предоставить новыйклассRandom.Щелкните на кнопке «Attach to Unity» ( вWindows, в macOS), чтобыприсоединить Visual Studio кUnity.Установите точкупрерывания встроке, добавленной в метод Start. Теперь вернитесь вUnityи запустите игру.Она должна прерваться сразу жепосле нажатия кнопки Play.Задержитеуказатель над Random.value(проследите за тем, чтобыонрасполагалсяименно надvalue).VisualStudioвыведет значение в подсказке: Оставьте среду Visual Studio присоединенной к Unity, затем вернитесь в ре- дакторUnityиостановитеигру(вредактореUnity,нев VisualStudio).Снова запустите игру. Проделайте это еще несколько раз. Каждыйразвы будете получать новое случайное значение. Такработает класс UnityEngine.Random: он выдает новое случайное значениеот0 до 1 при каждом обращениик его свойствуvalue. Нажмите кнопку Continue ( ), чтобы продолжить игру.Точка прерываниябыла установлена только в методе Start, который вызывается один раз для каждого экземпляра GameObject,поэтому по- вторного прерывания не будет. Вернитесь в Unity и остановите игру. Вынесможете редактировать сценарии в средеVisual Studio, пока она присоединена кUnity, поэтому щелкните на кнопке Stop Debugging, чтобы отсоединить отладчик Visual Studio от Unity. Оставьте среду Visual Studio присоединенной к Unity и несколько раз перезапустите игру. При каждом перезапуске вы будете получать новое случайное число в диапа- зонеот0до1. Возможно, Unity пред- ложит вам вкл юч ить отл ад ку, как это произошло в последней лабораторной работе Unity.
388 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 3 Преобразование объекта GameObject в заготовку В Unity заготовка (prefab) представляет собой объект GameObject, экземпляр которого можно создать в сцене. Внесколькихпоследних главах мы работали с экземплярами объектов и создавали объекты посредством создания экзем- пляров классов. Unity предоставляет возможность использо- ванияобъектовиэкземпляров, чтобы вымоглистроить игры, повторно использующие одни и те же объекты GameObject. Преобразуем объектGameObject в заготовку. УобъектовGameObjectестьимена. Переименуйте свой объект GameObject в OneBall.Для начала выделите сферу,щелкнув на ней в окнеHierarchy или в сцене. Затем в окнеInspector измените ее имя на OneBall. Теперь объектGameObject можно преобразоватьвзаготовку.ПеретащитеOneBallиз окнаHierarchy в папку Prefabs. Объект OneBall должен появиться в папке Prefabs. Обратите внимание: OneBall в окнеHierarchy окрашивается в синий цвет. Это означает, что он стал заготовкой (prefab).Для некоторых игр это нормально, но в игре, которуюмыстроим,все экземплярысфер должны создаватьсясценариями. ЩелкнитеправойкнопкоймышинаOneBallвокнеHierarchy,удалите объ- ект GameObject OneBall из сцены. Вы должны видеть его только вокне Project,но нев окнеHierarchy или всцене. А вы не забываете сохранять свою сцену в ходе работы? «Сохраняйтесь пораньше, сохраняйтесь почаще!» Когда объект GameObject окрашен в синий цвет в окне Hierarchy, Unity тем самым показывает, что перед вами экземпляр заготовки. Visual Studio не по- зволяет редактиро- вать код во время соединения с Unity. Если выпытаетесь редактировать код, но обнаруживаете, что Visual Studio не дает вносить изменения, скорее всего, среда Visual Studio все еще присоедине- на к Unity! Нажмите квадратную кнопку Stop Debugging, чтобы разорвать связь между ними. Также объект GameObject можно переименовать еще одним спосо- бом: щелкните на нем правой кноп- кой мыши в окне Hierarchy и выбе- рите команду Rename. Будьте осторожны!
Лабораторный курс Unity No3 Head First C# Лабораторный курсUnityNo 3 389 Создание сценария для управления игрой Игра должна каким-то образом добавлять шары в сцену(а такжевеститекущий счет и следить за тем, незавершилась лиона). Щелкнитеправойкнопкоймыши на папкеScripts вокнеProject исоздайтеновыйсценарий сименем GameController. В новом сценарии будут использоваться два метода, доступные в любом сценарии GameObject: Ì Метод Instantiateсоздает новый экземплярGameObject. ПрисозданииобъектовGameObject вUnityключевоеслово newобычнонеиспользуется(какэто делалось вглаве,например).Вместо этого вызывается метод Instantiate, который мыбудем вызывать из метода AddABall. Ì Метод InvokeRepeating снова иснова вызывает другойметодв сценарии. В данном случаеон ожидает 1.5секунды, азатемвызывает метод AddABallодин разв секундувсе оставшеесявремяигры. Исходный код выглядит так: public class GameController : MonoBehaviour { public GameObject OneBallPrefab; void Start() { InvokeRepeating("AddABall", 1 .5F , 1); } void AddABall() { Instantiate(OneBallPrefab); } } Метод с именем AddABall. Его единственная задача — создание экземпляра заготовки. К какому типу относится вто- рой аргумент, передаваемый InvokeRepeating? Мы передаем полеOneBallPrefab в пара- метре методаInstantiate, который будет использоваться Unityдля создания экземпляра вашей заготовки. Unity выполняет только сценарии, присоединенные к объектам GameObject в сцене. Сценарий GameController будет создавать экземпляры нашей за- готовки OneBall, но его необходимо к чему-то присоединить. К счастью, вы уже знаете, что камера представляет собой обычный объект GameObject с компонентом Camera (а также AudioListener). Главная камера (Main Camera) всегда доступна в сцене. Как вы думаете, как нам следует посту- пить с только что созданным сценарием GameController? Мозговой штурм Метод InvokeRepeating в Unity снова и снова вызывает дру- гой метод. В первом параметре передается строка с именем вызываемого метода.
390 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 3 Присоединение сценария к главной камере ЧтобысценарийGameControllerработал,егонеобходимо присоединить ккакому-тообъектуGameObject. Ксчастью, главная камера MainCamera тоже является объектом GameObject — просто этот объект снабжен компонентомCamera икомпонентом AudioListener,поэтомумыприсоединимновыйсценарий к главной камере. Перетащите сценарий GameController из папкиScripts в окне Project на главную камеру MainCamera вокнеHierarchy. ВзглянитенаокноInspector — выувидите внем компонентдля сценария(точно также,как для любого другогообъектаGameObject).Сценарийсодержит открытоеполе с именемOneBallPrefab, поэтому Unity отображает его в компоненте Script. У поляOneBallPrefab по-прежнему нетзначения(None), его необходимо задать. Перетащите объект OneBall из папкиPrefabs на полерядом с меткойOneBallPrefab. Теперь полеOneBallPrefab сценарияGameController содержит ссылку на заготовкуOneBall: Вернитесь к коду и внимательно присмотритесь к методу AddABall. Он вызывает метод Instantiate, передаваяемуваргументеполеOneBallPrefab.Выможетеинициализировать этополетак,чтобы внем храниласьнужнаязаготовка. Таким образом, каждыйраз,когда сценарийGameControllerбудет вызывать метод AddABall, он будет создаватьновый экземплярзаготовкиOneBall. Поле OneBallPrefab в классе GameController. Unity добавляет про- белы между буквами верхнего реги- стра, чтобы имя проще читалось (как и в предыдущей лабораторной работе). Об открытых и приватных полях вы узнали в главе5. Если класс сценария содержит от- крытое поле, то редактор Unity отображает это поле в компо- ненте Script в Inspector. Между буквами верхнего регистра он вставляет пробелы, чтобы имя проще читалось.
Лабораторный курс Unity No3 Head First C# Лабораторный курсUnityNo 3 391 Запустите свой код кнопкой Play Игра полностью готова к запуску.СценарийGameController, присоединенный кглавнойкамере, ожидает1.5 секунды,послечего создает экземпляр заготовки OneBall каждую секунду. Метод Start каждого созданного экземпляра OneBall перемещаетего вслучайную позицию всцене,а методUpdate поворачиваетего вокругосиY каждые 2 секунды с использованием полей OneBallBehaviour (как в предыдущей лабораторной работе). Проследите за тем, как игровая область постепенно заполняется вращающимися шарами. Unity вызывает метод Update каждого объекта GameOb ject перед каж ды м кадром. Это называется циклом обновления. Проследите за созданием экземпляров в окне Hierarchy Каждый шар, летающий в сцене, представляет собой экземпляр заготовкиOneBall. Каждыйизэкземпляровимеетсобственныйэкземпляр классаOneBallBehaviour. В окнеHierarchy можно отслеживать все экземпляры OneBall — при создании очередного экземпляра в Hierarchy добавляется новый пункт «OneBall(Clone)». Щелкнитеналюбом из пунктовOneBall(Clone),чтобы просмотреть его вокне Inspector. Выувидите,что его значенияTransform изменяютсявпроцессевраще- ния,как ив последнейлабораторнойработе. Разберитесь, как добавить поле BallNumber в сценарий OneBallBehaviour, чтобы, когда вы в сле- дующий раз щелкнете на экземпляре OneBall в окне Hierarchy и проверите его компонент One Ball Nehaviour (Script), под метками X Rotation, Y Rotation, Z Rotation и DegreesPer Second отображалось поле Ball Number: У первого экземпляра OneBall полю Ball Number должно быть присвоено значение 1. У второго экземпляра ему присва- ивается 2,у третьего — 3 и т. д. Подсказка: вам понадобится хранить счетчик, общийдля всех экземпляров OneBall. Измените метод Start, чтобыувеличить значение счетчика, а затем используйте его для присваивания полю BallNumber. Мы включили в ла- бораторный курс Unity ряд упражне- ний по программи- рованию. Они ничем не отличаются от упражнений, приве- денных в оставшей- ся части книги, — и помните, что подглядывать в ре- шение — не значит жульничать. Когда вы создаете экземпля- ры объектов GameObject в своем коде, они появля- ются в окне Hierarchy при запуске игры. Упражнение
392 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 3 Работа с экземплярами GameObject в окне Inspector Запустите игру. Послетого как внейбудут созданы несколько экземпляров шара, щелкнитена кнопке Pause — редактор Unity вернется к представлению Scene. Щелкните на одном из экземпляровOneBall в окнеHierarchy, чтобы выбратьего. Редактор Unity выделяетего вокне Scene,чтобы показать, какой объектбылвыбран. Перейдитек компонентуTransform вокнеInspectorизадайтеего свойствуZscale значение4,чтобы шар растянулся по оси. Снова запустите игру — теперь вы будете видеть, к какому шару применяются изменения. Попробуйте изменить значения его полей DegreesPerSecond, XRotation, YRotation и ZRotation, как в предыдущей лабораторной работе. Покаиграработает,попробуйте переключатьсямеждупредставлениями Gameи Scene. Вы можетеисполь- зовать манипуляторы впредставленииSceneдаже во время игрыидажедля экземпляровGameObject, созданныхметодом Instantiate(а не добавленныхв окноHierarchy). Попробуйте пощелкать на кнопке Gizmos в верхней части панели инструментов, чтобы включать и отключать манипуляторы. Вы можете включить их в представлении Game и одновременно от- ключить впредставлении Scene. Чтобы добавить в сценарий OneBallBehaviour поле BallNumber для хранения количества шаров, до- бавленныхранее, можно воспользоваться статическим полем (которое мы назвали BallCount).Каждый раз, когда создается новый экземпляр шара, Unity вызывает его метод Start; таким образом мы можем увеличитьстатическое поле BallCount и присвоить результат полюBallNumber этого экземпляра. static int BallCount = 0; public int BallNumber; void Start() { transform.position = new Vector3(3 - Random.value * 6, 3 - Random.value * 6, 3 - Random.value * 6); BallCount++; BallNumber = BallCount; } Все экземпляры OneBall совместно используют одно статическое поле BallCount, так что метод Start пер- вого экземпляра увеличивает его до 1, второй экземпляр увеличивает BallCount до 2, третий — до 3 и т. д. Упражнение Решение
Лабораторный курс Unity No3 Head First C# Лабораторный курсUnityNo 3 393 Предотвращение перекрытия шаров Авызаметили,что некоторыешары перекрываются друг другом? Unity содержит мощныйфизическийдвижок, с помощью которого выможете за- ставить своиобъектыGameObjectвестисебятак,словноониявляютсяреальными твердымителами, а твердыетела не могут занимать одну область пространства. Чтобы предотвратить перекрытие, необходимо сообщить Unity, что заготовка OneBall является твердым объектом. Остановитеигруищелкнитена заготовкеOneBallв окнеProject, чтобывыделить ее. Перейдите к окну Inspector и прокрутитесписок до кнопки Add Component: Щелкнитенакнопке,откроетсяокноComponent. ВыберитеPhysics,чтобы просмотреть физические компоненты, а затем выберитекомпонентRigidbody,чтобы добавить его ккомпоненту. Проследите за тем, чтобы флажок UseGravity был снят. В противном случае шары будут реагировать на гравитацию и начнут падать. А поскольку их ничто не остановит, падение будет длиться вечно. Сновазапуститеигру; выувидите,что шарыв нейуженеперекрываются. Времяот временишар можетбыть созданповерхужесуществующего.Когда это происходит, новыйшар «сталкивает» старыйсо своего места. Проведем небольшой физический эксперимент, который докажет, что шары действительно стали твердыми телами. Запустите игру и подождите, когда будут созданы более двух шаров. Перейдитев окно Hierarchy.Если оно имеет вид: значит, вы редактируетезаготовку — щелкните на кнопке со стрелкой назад ( ) в правом верхнемуглу окна Hierarchy, чтобы вернуться к сцене (возможно, вам придется снова развернуть SampleScene). Ì Удерживая клавишуShift, щелкните на первом экземпляреOneBall в окне Hierarchy, а затем щелкните на втором. В результате должны быть выделены два экземпляра OneBall. Ì В поляхPosition на панели Transform выводятся дефисы( ).Задайте в поле Position значение (0, 0, 0), чтобы задать положение экземпляров OneBall одно- временно. Ì Используя Shift+щелчок для выделения других экземпляров OneBall, щелкните правойкнопкоймышиивыберитеDelete, чтобыудалить ихизсцены, вкоторой должны остаться только два перекрывающихся шара. Ì Снимите игруспаузы —шарытеперь не могут перекрываться, поэтому онибудут вращаться рядом другс другом. Остановите игру в Unity и Visual Studio, сохраните сцену. «Сохраняйтесь пораньше, сохраняйтесь почаще!» Раз уж мы заня- лись физическими экспериментами, есть один опыт, который бы оце- нил Галилей. По- пробуйте уста- новить флажок Use Gravity во время выполне- ния игры. Новые шары, которые будут созда- ваться в сцене, начнут падать вниз, время от времени сталки- ваясь с другими шарами и сбивая их с места. Окно Hierarchy может ис поль зо- ваться для уда- ления объектов GameObject из сцены во время выпол нен ия игры.
394 https://github.com/head-first-csharp/fourth-edition Лабораторный курс Unity No 3 ¢ Альбедо — термин из области физики, обозначающий цвет, отражаемый объектом. Unity может использовать текстурные карты с альбедо в качестве материалов. ¢ Unity использует собственный класс Random в простран- стве имен UnityEngine. Статический метод Random.value возвращает случайное число вдиапазоне от 0 до 1. ¢ Заготовка (prefab) представляет собой объект GameObject, экземпляр которого вы можете создать в своей сцене. Любой объект GameObject может быть преобразован в заготовку. ¢ Метод Instantiate создает новый экземпляр объекта GameObject. Метод Destroyуничтожает его. Экземпляры создаются и уничтожаются в конце цикла обновления. ¢ МетодInvokeRepeating вызывает другой метод из сце- нария снова и снова. ¢ Unity вызывает метод Update каждого объекта GameObject перед каждым кадром. Это называется ци- клом обновления. ¢ Вы можете просмотреть «живые» экземпляры своих за- готовок, щелкая на них в окне Hierarchy. ¢ Когда вы добавляете компонент Rigidbody в объект GameObject, физический движок Unity заставляет его вести себя как реальный твердый физический объект. ¢ Компонент Rigidbody позволяет включать и отключать гравитациюдля объектов GameObject. КЛЮЧЕВЫЕ МОМЕНТЫ Проявите фантазию! Мынаходимся ужена серединепостроения игры!Онабудетзавершена в следующей лабораторнойра- боте. А пока перед вами открылась отличная возможность потренироваться в построениибумажных прототипов. Описаниеигрыбыло приведено вначалеэтойлабораторнойработы. Попробуйтесоздать ее бумажный прототип. А может, у вас будут какие-нибудь предложения, которые сделают игру более интер ес но й? 12 PLAY AGAIN 8 Нарисуйте фон сцены Unity на листе бумаги, затем изобра- зите бильярдные шары на от- дельных клочках. Кнопка Play Again появляется после завершения игры. Предложите, как использовать шар с цифрой 8 из первых двух лабо- раторных работ, чтобы игра была более интересной. Сможете?
Интерфейсы, приведение типов и «is» 7 Классы должны держать обещания Ладно-ладно. Я знаю, что я реализую интерфейс Букмекер. Но у меня не хватает времени, и я не смогу реализовать метод ВыплатаДенег() до следующей пятницы. У тебя два дня. А потом я пошлю к тебе пару объектов Бандит, чтобы убедиться, что ты реализуешь метод ХожуНаКостылях. Вам нужен объект для выполнения конкретной задачи? Используйте ин- терфейс. Иногда возникает необходимость сгруппировать объекты по выполняе- мым ими функциям, а не по классам , от которых они наследуют. На помощь приходят интерфейсы. Интерфейсы могут использоваться для определения конкретных задач. Любой экземпляр класса, реализующего интерфейс, гарантированно выполняет эту задачу независимо от того, с какими другими классами он связан. Чтобы эта схема ра- ботала, каждый класс, реализующий интерфейс, должен гарантировать выполнение всех своих обязательств... иначе программа компилироваться не будет.
396 глава 7 включение нового класса в иерархию классов пчел Улей под атакой! Вражескийулей пытается захватить территорию пчелиной матки и посылаетбое- выхпчел, которыенападаютна вашихрабочих. Из-за этого в иерархию классовбыл добавлен новый элитный субкласс Bee с именем HiveDefender; пчелы этого класса занимаются защитойулья. О б ъ е к т Q u e e n О б ъ е к т Hi v e D e f e n d e r За щища ть улей любой ценой. Да, повелительница! Нам понадобится метод DefendHive, потому что враги могут напасть в любой момент Можно добавить в иерархию классов Bee субкласс HiveDefender; для этого мы расширим класс Bee, пере- определимCostPerShiftколичествоммеда, потребляемого каждым защитником за смену, и переопределим метод DoJob так, чтобы пчела летела к вражескомуулью и на- падала на вражескихпчел. Новражескиепчелы могутнапасть влюбоймомент. Мы хотим, чтобы защитники могли защищатьулей независимо от того, выполняют онисвое нормальное задание или нет. Из-за этого в дополнение к DoJob мы добавим метод DefendHiveк каждому классуBee,способномузащищать улей, — не только к классуэлитныхрабочихHiveDefender, ноиковсемегобратьямисестрам, способным защищать матку.Маткавызывает методыDefendHive своихрабочих каждыйраз,когда она видит,что улейатакуют. Hi veDef en der overridefloatCostPerShift protec ted over ri de DoJ ob DefendHi ve NectarCollector overridefloatCostPerShift protec ted ov erri de DoJ ob Bee stringJob virtualfloatCostPerShift (read-only) W ork TheN extS hi ft pr otected v i rtual DoJo b Nect ar Def en der DefendHi v e
дальше 4 397 приведение типов интерфейсов и is Мы можем воспользоваться приведением типов для вызова метода DefendHive... Когда мы программировали метод Queen.DoJob, мы использовали цикл foreach для получения всех ссылок Bee в массиве workers. Далееэти ссылки использовались для вызова worker.DoJob. Еслиулей атаккуют, матка Queen хочет вызвать методы DefendHive своих защитников. Мы предоставим метод HiveUnderAttack, которыйбудет вызыватьсякаждыйраз, когдаулейподвергаетсянападению вражеских пчел. Матка в цикле foreach приказываетрабочим защищать улей, пока все нападающие не исчезнут. Нотутвозникаетпроблема. Матка может использовать ссылкиBee для вызова DoJob,потому что каж- дый субкласс переопределяет Bee.DoJob,но она не можетиспользовать ссылку Beeдлявызова метода DefendHive,потому что этотметоднеявляется частью класса Bee. Как жеей вызватьDefendHive? ТаккакDefendHive определяется только в субклассах, для вызова метода DefendHiveнам придется вы- полнить приведение типа для преобразованияссылкина Bee к правильному субклассу: public void HiveUnderAttack() { foreach (Bee worker in workers) { if (EnemyHive.AttackingBees > 0) { if (worker.Job == "Hive Defender") { HiveDefender defender = (HiveDefender) worker; defender.DefendHive(); } else if (worker.Job == "Nectar Defender") { NectarDefender defender = (NectarDefender) defender; defender.DefendHive(); } } } } . .. но что, если потребуется добавить новые субклассы Bee, способные защищать улей? Некоторые производители меда и опекуны тоже хотят встать в строй и защищать улей. Это означает, что нам придется добавить но- вые блоки else в свой метод HiveUnderAttack. Архитектураусложняется. МетодQueen.DoJob остается простым иудобным — очень корот- кийцикл foreach, которыйиспользует модель классов Bee для вызова конкретной версии метода DoJob,реализованной всубклассе. Но с методом DefendHive это невозможно, потому что онне является частью класса Bee — и мы не хотим добавлять его, потому что не все пчелы могут защищать улей. Существует ли более эффективный способ выполнения одной задачи классами, не связанными отношениями наследования? Egg Car e overridefloatCostPerShift protec ted ov err ide DoJ ob EggDefender DefendH iv e HoneyManufacturer overridefloatCostPerShift protec ted over ri de DoJ ob Ho neyD ef end er DefendHi ve Производители меда и опе- куны тоже хотят участво- вать в защите улья. При- дется ли Queen создавать if/ else для каждого субкласса?
398 глава 7 интерфейсы для задач Интерфейс определяет методы и свойства, которые должны быть реализованы классом... Интерфейсработаетпрактическитакже,как абстрактныйкласс: вы используетеабстрактныеметоды, а затем двоеточие(:),чтобы классреализовал этот интерфейс. Таким образом, если вы хотите добавить защитников улья, для этого можно определить интерфейс с именемIDefend.Ниже показано, какэтовыглядит. Ключевоесловоinterface определяетинтерфейс, в который входит единственный компонент — абстрактный метод с именем Defend.Все компоненты интерфейса поумолчанию являются открытыми иабстрактными, поэтому C# для простоты позволяет опустить ключевыесловаpublic иabstract. interface IDefend { v oid Defe nd(); } Любой класс, реализующийинтерфейсIDefend, должен включать методDefend, объявлениекоторого соответствуетобъявлению в интерфейсе. Еслионэтого несделает,компилятор выдаетошибку. . .. н о количество интерфейсов, которые могут быть реализованы классом, не ограничено Мыужесказали, что для реализации интерфейса классом используется двоеточие(:).А что,есликласс ужеиспользует двоеточие длярасширениябазового класса?Никаких проблем!Классможетреализовать много разных интерфейсов, даже еслион ужерасширяетбазовый класс: c lass NectarD efender : NectarC olle ctor , ID efend { void Defend() { /* Код защиты улья*/ } } Итак, теперь у нас имеется класс, которыйработает какNectarCollector,но также можетзащищать улей.NectarCollector расширяетBee, так что при ис- пользовании его по ссылке на BeeондействуеткакBee: Bee worker = new NectarCollector(); Console.WriteLine(worker.Job); worker.WorkTheNextShift(); НоприиспользованиипоссылкенаIDefendон действует какзащитникулья: IDefend defender = new NectarCollector(); defender.Defend(); Если класс реали- зует интерфейс, он должен включать все методы и свой- ства, перечислен- ные в интерфейсе; в противном случае код не построится. Интерфейс содержит один компо- нент — открытый абстрактный метод с именем Defend. Он работает точно так же, как абстрактные ме- тоды, приведенные в главе 6. Так как метод Defend является частью интерфейса IDefend, класс NectarDefend обязан реализовать его; в противном случае он не будет компилироваться.
дальше 4 399 приведение типов интерфейсов и is О б ъ е к т Q u e e n Мыприведем много примеров интерфейсов. Всееще не до конца понимаете, как рабо- тают интерфейсы и почему их стоит ис- пользовать? Небеспокойтесь,это нормаль- но! Синтаксис интерфейсов несложен, но в нем много нюансов. Поэтому мы посвя- тим интерфейсам дополнительное время... рассмотрим множество примеров и потре- нируемся на многочисленных упражнениях. IDefender D efendHi ve Bee stringJob virtualfloatCostPerShift ( read- onl y) W ork TheNex tS hift protec ted v ir tual D oJob Hi veDef en der overridefloatCostPerShift protec ted ov erri de DoJ ob DefendHi v e NectarCollector overridefloatCostPerShift protec ted over ri de DoJ ob Nect ar Def en der DefendHi v e Интерфейсы позволяют несвязанным классам выполнять одну задачу Интерфейсы—чрезвычайномощныйинструмент дляпроектирования кода C#, простогодляпонимания ипостроения. Дляначала продумайте конкретныезадачи, которыедолжны выполняться классами, потому что именно вних заключаетсясуть интерфейсов. Теперь я знаю, что ты можешь защитить улей, и всем нам стало гораздо безопаснее! Как же это помогает пчелиной матке? Интерфейс IDefender существует за пределами иерархии классовBee. Такимобразом,мыможем добавить класс NectarDefender, которыйумеет защищать улейиможетрасширятьNectarCollector. Queen мо- жет поддерживатьмассиввсехсвоихзащитников: IDefender[] defenders = new IDefender[2]; defenders[0] = new HiveDefender(); defenders[1] = new NectarDefender(); Втакомслучаеона сможетлегко призвать своих з а щитнико в: private void DefendTheHive() { for each (IDe fend er d efen der in de fend ers) { defe nder .Def end( ); } } Ипосколькуэтафункциональность существует за пределами модели классов Bee, это можносделать без изменения существующего кода. Любой класс Bee может реализовать интерфейс IDefender независимо от своего положения в иерар­ хии классов. Если он со- держит метод DefendHive, код успешно построится. NectarDefender не наследует от HiveDefender, но так как они оба реализуют ин- терфейс IDefender, мы можем соз- дать массив, со- держащий ссылки на обе разновидно- сти объ ект ов. Реализация интерфейса обо- значается на диаграмме клас- сов пунктирными линиями. РЕЛАКС
400 глава 7 клоуны и интерфейсы Лучший способразобратьсявинтерфейсах — начать пользоваться ими. Создайте новый проект кон- сольного приложения. Добавьте метод Main. В следующем коде определяется класс TallGuy, а также код Main, который создает его экземпляр с использованием инициализатора объекта и вызывает его метод TalkAboutYourself.Ничего нового в этом коде нет: class TallGuy { public string Name; public int Height; public void TalkAboutYourself() { Console.WriteLine($"My name is {Name} and I'm {Height} inches tall."); } } class Program { static void Main(string[] args) { TallGuy tallGuy = new TallGuy() { Height = 76, Name = "Jimmy" }; tallGuy.TalkAboutYourself(); } } Добавьте интерфейс. Мы заставим класс TallGuy реализовать интерфейс. Добавьте в проект интерфейс IClown: щелкните правой кнопкой мыши на проекте в окне Solution Explorer, выберите Add>>New Item... (Windows) илиAdd>>New File... (Mac) и выберите команду Inter- face. Проследите за тем, чтобыфайлу было присвоено имяIClown.cs. IDE создаст интерфейс, которыйвключаетобъявлениеинтерфейса. Добавьте методHonk: interface IClown { void Honk(); } Попробуйте запрограммировать остальной код интерфейса IClown. Прежде чем переходить к следующему шагу, попробуйте создать остальную часть интерфейса IClown и измените класс TallGuy для реализации этого интерфейса. Помимо void-метода с именем Honk, не получающего параметров, интерфейс IClown также должен содержать доступное только для чтениясвойство с именемFunnyThingIHave, котороеимеетget-метод,но неимеет set-метода. 1 2 3 Добавлять public или abstract в интерфейс не нужно, потому что все свойства и ме- тоды автоматически становятся откры- тыми и абстрактными. Потренируемся в использовании интерфейсов Имена интерфейсов начинаются с I Когда вы создаете интерфейс, присвойте ему имя, начинающееся с буквы I верхнего регистра. Нет никаких правил, которые бы это требовали, но при со- блюдении этого соглашения ваш код станет намного более понятным. В этом несложно убедиться: перейдите в IDE в любую пустую строку в любом методе и введите «I» — в окне IntelliSense выводится список интерфейсов .NET. Сделайте это !
дальше 4 401 приведение типов интерфейсов и is Ниже приведен интерфейс IClown. Выправильно написаликод? Если выразместилиметод Honkна первомместе,это нормально —в интерфейсах,как ивклассах,порядоккомпонентов интерфейса роли не играет. interface IClown { string FunnyThingIHave { get; } void Honk(); } Измените класс TallGuy, чтобы он реализовал IClown. Напоминаем: за оператором : всегда указываетсябазовый класс, от которого наследует класс (если он есть), а затем идет список интерфейсов, которые онреализует(интерфейсыразделяются запятыми).Так как в данном случаебазового класса нетиреализуется всего одининтерфейс,объявление выглядиттак: class TallGuy : IClown Убедитесь втом,чтоостальнойкодклассавыглядит также, включаядва поля иметод. Выберите команду BuildSolution из меню Build вIDE, чтобы откомпилировать и построить программу. Выполучитедвеошибки: Исправьте ошибки, добавив недостающие компоненты интерфейса. Ошибки исчезнут, как только вы добавите все методы и свойства, определенные в интерфейсе. Реализуйте ин- терфейс: добавьте строковоесвойствоFunnyThingsIHave,доступное только для чтения,и get- метод, который всегда возвращает строку «big shoes». Затем добавьте метод Honk, который выводит на консоль сообщение «Honk honk!». Код долженвыглядеть так: public string FunnyThingIHave { get { return "big shoes"; } } public void Honk() { Console.WriteLine("Honk honk!"); } Теперь ваш код компилируется. Обновите метод Main, чтобы он выводил свойство FunnyThingsIHave объекта TallGuy, а затем вызывал свой метод Honk. static void Main(string[] args) { TallGuy tallGuy = new TallGuy() { Height = 76, Name = "Jimmy" }; tallGuy.TalkAboutYourself(); Console.WriteLine($"The tall guy has {tallGuy.FunnyThingIHave}"); tallGuy.Honk(); } 4 5 6 7 ИнтерфейсIClown требует, чтобы лю- бой реализующий его класс содержал void-метод с именем Honk и строковое свойство с именем FunnyThingIHave, имеющееget-метод. Любой класс, реализующий интерфейс IClown, обязан содержать void-метод с именем Honk и строковое свойство с име- нем FunnyThingsIHave, имеющее get-метод. Свойство FunnyThingsIHave также может иметь set-метод.В интерфейсе о нем ничего не сказано, поэтому возможны оба варианта.
402 глава 7 ри суем ин терф ей сы Вам предоставляется возможность продемонстрировать свои худо- жественные способности. Слева приводятся объявления интерфейсов иклассов.Ваша задача— нарисовать соответствующуюдиаграмму клас- са справа. Не забудьте обозначать реализацию интерфейса пунктирной линией, анаследованиеот класса —сплошной. Вы получаете... Как это выглядит? 1) interface Foo { } classBar:Foo{} 2) interface Vinn { } abstract class Vout : Vinn { } 3) abstract class Muffie : Whuffie { } class Fluffie : Muffie { } interface Whuffie { } 4) class Zoop { } class Boop : Zoop { } class Goop : Boop { } 5) class Gamma : Delta, Epsilon { } interface Epsilon { } interface Beta { } class Alpha : Gamma,Beta { } class Delta { } (interface) Foo Bar 1) 2) 3) 4) 5) Мы нарисовали первую диаграмму за вас. Для 5) вам понадобится чуть больше места Возьми в руку карандаш
дальше 4 403 приведение типов интерфейсов и is Слева изображены наборы диаграмм классов. Ваша задача — преобразовать их в допустимые объявления C#.Мы выполнили упражнение 1 за вас. А вы заметили, что объявления классов представляют собой пары фигурных ско- бок {}? Дело в том, что классы пока не содержат компонентов. (Но при этом они остаются действительнымиклассами,которые успешностроятся!) Вы получаете... Как выглядит объявление? Click Top Fee Clack Tip Fi Foo Bar Baz Zeta Beta A lpha Delta 1 2 3 4 5 1) 2) 3) 4) 5) public class Click { } public class Clack : Click { } C lack C lack C lack класс ин терф ейс абстрактный класс расширяет реализует Ñ Возьми в руку карандаш
404 глава 7 абстрактные классы и интерфейсы Как это выглядит? (interface) Vinn 2) Vout 3) Fluffie (interface) Whuffie Muffie 4) Boop Goop Zoop 5) (interface) Epsilon (interface) Beta Alpha Delta Gamma Кто важнее: абстрактный класс или ин терфейс? Абстрактный класс: Ядумаю,вполнеочевидно,кто из нас важнее. Без меня программист не выполнит свою работу.По- смотрим правде в глаза: ты и близко ко мне не можешь подойти. Как ты вообще мог подумать, что можешь быть важнееменя?Ты даженевсостояниинаследовать как полагается,тебя можно толькореализовать. Превосходит?Датысошелсума.Янамногоболее гибок. Да,мои экземпляры немогутсоздаваться— но этого неможешь и ты. Зато, вотличиеоттебя, я могу пользоваться невероятной мощью насле- дования. Нищеброды, которые тебярасширяют, даже не могут пользоваться ключевыми словами virtual и override! Интерфейс: Прекрасно. Другогояоттебя и неожидал! Отлично, вечная история. «Интерфейсы не ис- пользуют механизм наследования», «интерфейсы только реализуются». Все это обычное невежество. Реализацияничемне хуже наследования, а в чем-то даже превосходит его! Да?А есликлассзахочет наследовать утебяиутвое- готоварища? Наследовать отдвухклассовнельзя. Нужновыбратького-то одного. А вот числореали- зуемыхинтерфейсов можетбытьлюбым, такчтоне нужно говорить мне о гибкости!С моей помощью программист заставит классы делать что угодно. Беседа у камина Возьми в руку карандаш Решение
дальше 4 405 приведение типов интерфейсов и is Как выглядит объявление? abstract class Top { } classTip:Top{} 2) abstract class Fee { } abstract class Fi : Fee { } 3) interface Foo { } classBar:Foo{} classBaz:Bar{} 4) interface Zeta { } class Alpha : Zeta { } interface Beta { } class Delta : Alpha, Beta { } 5) Абстрактный класс: Кажется, ты несколько переоцениваешь своюроль. И ты думаешь, что это хорошо?Ха!Когда ты ис- пользуешь меняимоисубклассы, тыточнознаешь, что происходит внутри всех нас. Я могуреализо- вать любое поведение, необходимое всем моим субклассам,а им остается только наследовать его. Прозрачность — мощная штука! Дану? По моим наблюдениям, программистов оченьдаже волнуетсодержимое свойств и методов. Да, конечно... расскажи кодеру, что он не может писать код. Интерфейс: Серьезно?Ну давай посмотрим, насколько мощ- ным я могу быть для разработчиков, которые используют меня. Вся моя суть — выполнение конкретнойзадачи.Еслиуразработчика имеется ссылкана интерфейс, емувообще ненужноничего знать о том,что происходит внутриобъекта. Девятьиздесяти,что программисту нужныопре- деленныесвойства и методы,но его при этом не волнует, какименно ониреализуются. Да,конечно.Когда-нибудь потом.Тольковспомни, как часто программисты пишут методы с объ- ектами в качестве параметров, которые просто должны включать в себя определенные методы. И в этот момент никого не волнует, как именно этиметодыпостроены.Главное, чтобы онибыли. Программисту достаточно воспользоваться интер- фейсом, ипроблемарешена! Ичто ты с ним будешь делать? Delta наследу- ет от Alpha и реализует Be ta. Возьми в руку карандаш Решение
406 глава 7 интерфейсы не создают объектов Создать экземпляр интерфейса невозможно, но можно получить ссылку на интерфейс Допустим, вам нужен объект с методом Defend, чтобы вы могли использовать его в цикле для защиты улья. Для этого подойдет любой объект, реализующий интерфейс IDefender. Это можетбыть объектHiveDefender, объектNectarDefenderилидаже совершеннопосторонний объектHelpfulLadyBug.Если объектреализует интерфейсIDefender,он гарантированно со- держитметод Defend.Вам остается только вызвать его. Издесь вигрувступают ссылкина интерфейсы. Вы можете воспользоватьсятакойссылкой для обращения к объекту, которыйреализует нужный вам интерфейс, и всегда можете быть увереныв том, что он содержит нужные вам методы,даже есливыне знаете о нем ничегоболее. При попытке создания экземпляра интерфейса код не будет строиться Выможетесоздать массивссылокIWorker, носоздатьэкземпляр интерфейса невозможно.Вы можете связать эти ссылкис новыми экземплярами классов, реализующих IWorker. Таку вас появляется массив,содержащий объектыразныхвидов! При попытке создания экземпляра интерфейса компилятор протестует: IDefender barb = new IDefender(); Вы не сможете использовать ключевое слово new с интерфейсом, и это логично — методы и свойства не имеютреализаций.Еслибы вы создали объект набазеинтерфейса,то какбы определялось поведениетакого объекта? Используйте интерфейсы для ссылок на уже существующие объекты Итак,создать экземпляринтерфейса невозможно... Но интерфейсможноиспользовать для создания ссылочной переменнойииспользовать этупеременную дляобращения кобъекту, реализ ующе му интер фей с. Помните,чтоссылкуTigerможно передать любомуметоду,которыйрассчитываетполучить Animal,потому что TigerрасширяетAnimal? Здесь та же самая ситуация— экземпляр класса, реализующего IDefender,можетиспользоваться влюбом методе иликоманде,рассчитываю- щем получить IDefender. IDefender susan = new HiveDefender(); IDefender ging er = new Nect arDefend er(); Этообычныекомандыnew —точнотакиеже,какте,чтовстречались вамранеевкниге. Един- ственное различиезаключается в том, что для обращения кним используется переменная типа IDefender. И хотя этот объ ект способен на большее, при использовании ссылки на интерфейс вам до- ступны только методы, входящие в интерфейс. НЕКОМПИЛИРУЕТСЯ О б ъ е к т N e c t a r Def e n d e r О б ъ е к т H i v e D efe n d e r susan g i n g e r Интерфейс используется для объявления переменных «susan» и «ginger», но это обычные ссылки, которые работают точно так же, как любые другие.
дальше 4 407 приведение типов интерфейсов и is Каждый фрагмент кода можно использовать несколько раз. intEar() this this . fa ce this .f ace : ; class abstract interface Acts( ); INose( ); Of76( ); Clowns( ); Picasso( ); A cts INo se Of2016 Clowns Picasso i i() i( x) i[ x] i.Ear(x) i[x].Ear() i[x].Face() i[x].Face Of76 [ ] i= newINose[3]; Of76[3]i; INose[]i= newINose(); INose [ ] i= newINose[3]; class 5 class 7 class 7 public class Точка входа — перед вами п олн оце нная программа C# get set re turn У бассейна Возьмите фрагментыкодаизбассейнаи разместите их в пустых строках. Каждыйфрагмент можно использовать несколькораз. Вбассейнеестьилишние фрагменты.Ваша задача —получить набор классов, которые будут компилироваться, запускаться идавать показанныйниже результат. class A cts : Picasso { public Acts() : base("Acts") { } public override int Ear() { return 5; } } class Of2016 : Clowns { public override string Face { get { return "Of2016"; } } public static void Main(string[] args) { string result = ""; INose[] i = new INose[3]; i[0] = new Acts(); i[1] = new Clowns(); i[2] = new Of2016(); for(intx=0;x<3;x++){ result += $"{ i[x].Ear() } { i[x].Face }\n"; } Console.WriteLine(result); Console.ReadKey(); } } interface INose { int Ear() ; string Face { get; } } abstract class Picasso : INose { private string face; public virtual string Face { get { return face ;} } public abstract int Ear(); public Picasso(string face) { this.face = face; } } class Clowns : Picasso { public Clowns() : base("Clowns") { } public override int Ear() { return 7; } } 5 Acts 7 Clowns 7 Of2016 Результат
408 глава 7 ссылки на интерфейсы class Acts : Picasso { public Acts() : base("Acts") { } public override int Ear() { retu rn 5; } } class Of2016 : Clowns { pub lic o verri de st ring Face { get { return "Of2016"; } } pub lic s tatic void Main (stri ng[] args) { stri ng re sult = ""; INose[] i = new INose[3]; i[0] = new Acts(); i[1] = new Clowns(); i[2] = new Of2016(); for(intx=0;x<3;x++){ resul t += $"{ i[x].Ear() } { i[x].Face }\n"; } Cons ole.W riteL ine(r esult ); Cons ole.R eadKe y(); } } interface INose { int Ear() ; string Face { get; } } abstract class Picasso : INose { private string face; p ublic virt ual s tring Face { get { return face ;} } p ublic abst ract int E ar(); p ublic Pica sso(s tring face ) { this.face = face; } } class Clowns : Pi ca sso { p ublic Clow ns() : bas e("Cl owns" ) { } p ublic over ride int E ar() { return 7; } } Здесь класс Acts вызывает конструктор из класса Picasso, от которого он наследует. Конструктору передается значение "Acts", которое сохраняется в свойстве Face. Face — get­метод, возвращающий значение свойства face. Оба компонен- та определяются в Picasso и наследу- ются субклассами. У бассейна. Решение Возьмите фрагментыкодаиз бассейна иразместитеихв пустых строках. Каждыйфрагмент можноиспользовать несколькораз. В бассейне есть и лишние фрагменты. Ваша задача — полу- чить набор классов, которые будут компилироваться, запу- скаться идавать показанныйниже результат. 5 Acts 7 Clowns 7 Of2016 Результат intEar() th is this. face this.face : ; class ab st ract interface Acts( ); INose( ); Of76( ); Clowns( ); Picasso( ); Ac ts INos e Of2016 Clowns Picasso i i() i( x) i[ x] i.E ar( x) i[x].Ear() i[x].Face() i[x].Face Of76 [ ]i = new INose[3]; Of76[3]i; INose[]i= new INose(); INose [ ] i= new INose[3]; class 5 class 7 class 7 public class get set re turn
дальше 4 409 приведение типов интерфейсов и is Ссылки на интерфейсы являются обычными ссылками на объекты Вы уже знаете, что все объекты существуют в куче. Когда вы работаете со ссылкой на интерфейс, она всего лишь становит- сяновымспособом обращения кобъектам,с которымивыуже работали. Давайте повнимательнее присмотримся к тому, как интерфейсы используются для обращения к объектам из кучи. Все начинается с обычного создания объектов. Сле- дующий код создает экземпляр HiveDefender и экзем- пляр NectarDefender— оба этихклассареализуют интер- фейс I D efender. HiveDefender bertha = new HiveDefender(); NectarDefender gertie = new NectarDefender(); Затем добавляются ссылки на IDefender. Ссылки на ин- терфейсы используются точно так же, как ссылки на лю- бые другиетипы.Следующие две команды используют ин- терфейсыдлясозданияновых ссылок на существующие объекты. Ссылка на интерфейс может указывать только на экземпляр класса, который этот интерфейсреализует. IDefender def2 = gertie; IDefender captain = bertha; Ссылка на интерфейс поддерживает существование объекта. Если в системе не осталось ни одной ссылки, указывающей на объект, этот объект уничтожается. Нет никаких правил, требующих, чтобы ссылки относились кодномутипу!Втом,что касаетсяжизниобъектов,ссыл- ка на интерфейс ничем не отличается от любых других ссылок, поэтому она предотвращает уничтожение объ- екта входе сборкимусора. bertha = gertie; // Ссылка captain все еще указывает на объект // HiveDefender Интерфейс используется так же, как любой дру- гой тип. Вы можетесоздать новыйобъекткомандой new и присвоить его интерфейсной ссылочной пе- ременной воднойстрокекода. Интерфейсы могут использоваться для создания массивов, которые содержатлюбыеобъекты,реализующиеинтерфейс. IDefender[] defenders = new IDefender[3]; defenders[0] = new HiveDefender(); defenders[1] = bertha; defenders[2] = captain; 1 2 3 4 Этот объект не исчеза- ет из кучи, потому что «captain» продолжает ссылаться на него. Теперь bertha указывает на NectarDefender. О б ъ е к т N e c t a r De f e n d e r О б ъ е к т H i v e Def e n d e r g ertie b e rt h a О б ъ е к т N e c t a r De f e n d e r О б ъ е к т H i v e D ef e n d e r g ertie b e rt h a def2 c a p t a i n О б ъ е к т N e c t a r Def e n d e r О б ъ е к т H i v e D ef e n d e r ger tie def 2 c a p t a i n О б ъ е к т N e c t ar D e f e n d e r b e rt h a О б ъ е к т H i v eDe f e n d e r gertie def2 b e rt h a О б ъ е к т H i v eDe f e n d e r c a p t a i n
410 глава 7 использование интерфейсадля определения задачи объекта RoboBee 4000 может выполнять работу пчел без расхода драгоценного меда Впоследнемкварталебизнеспроцветал,и упчелинойматки хватилосредств дляприобретенияпоследнеготехнологического достижения:RoboBee4000. Роботумеет выполнятьработутрехразныхпчел,а самое замечательное,что он не расходуетмед!Впрочем,есть некоторыепретензии кэкологичности — роботрасходуетбензин. Как же при помощиинтерфейсов интегрировать RoboBeeв повседневнуюработуулья? class Robot { public void ConsumeGas() { // Неэкологично } } class RoboBee4000 : Robot, IWorker { public string Job { get { return "Egg Care"; } } public void WorkTheNextShift() { // Выполняет работу трех пчел } } Robot ConsumeGas I Wo rker Job WorkTheNextShift Ro bo Bee Job WorkTheNextShift Bee Job abstract CostPerShift WorkTheNextShift abstract DoJob Остаетсяизменитьсистемууправленияульем так,чтобы вкаждом обращении крабочему вней исполь- зовался интерфейс IWorker вместо абстрактного классаBee. Начнем с простейше- го класса Robot — все знают, что роботы работают на бен- зине, поэтому класс содержит метод ConsumeGas. Класс RoboBee реали- зует оба компонента интерфейса IWorker. В этом отношении у нас нет выбора — если класс RoboBee не реализует все содержимое интер- фейса IWorker, код ком- пилироваться не будет. Класс Bee реализует интерфейс IWorker, тогд а как кл асс RoboBee наследует от Robot и реализует IWorker. Это озна- чает, что объект является роботом, но может выполнять за- дачи рабочих пчел. Можно создать интерфейс IWorker, который состоит из двух компонен- тов, относящихся к выполнению рабо- ты в улье. Повнимательнееприсмотритесь к диаграмме классов, чтобы по- нять, как использовать интер- фейс для интеграции класса RoboBee в систему управления ульем. Помните: пунктирные линии используются для обозна- чения того, что объект реализует ин тер фей с.
дальше 4 411 приведение типов интерфейсов и is Измените систему управления ульем так, чтобыдля обращений к рабочим в ней использовал- ся интерфейс IWorker вместо абстрактного класса Bee. Ваша задача — добавить в проект интерфейс IWorker, а затем провести рефакторинг кода, чтобы заставить класс Bee реализовать его, а также изменить класс Queen, чтобы в нем использовались только ссылки IWorker. Обновленная диаграмма классов должна выглядеть так: NectarCollector overridefloatCostPerShift protec ted ov erri de DoJ ob HoneyManufacturer overridefloatCostPerShift pr otected ov err ide D oJob E ggCa re overridefloatCostPerShift pr otected ov err i de DoJob Queen str ing S tatus Report (r ead-o nly ) overridefloatCostPerShift privateBee[]workers AssignBee CareF orE ggs protec ted over ri de DoJ ob IWorker stringJob voi d Wor kT heNex tS hift Bee stringJob virtual floatCostPerShift (r ead-on ly ) voi d Wor kT heNex tS hift protec ted vi rtual DoJ ob Когда класс Bee реализует интерфейс IWorker, все его субклассы также будут автоматически реализовать I Wo rker. Что нужно сделать: • Добавьте интерфейс IWorker в проект системы управления ульем. • Измените класс Bee, чтобы он реализовал интерфейс IWorker. • Измените класс Queen, чтобы все ссылки на Bee были заменены ссылками на IWorker. Вроде бы работы не так уж много, потому что... ее действительно немного. После добавления интерфейсов остается только изменить одну строку кода в классе Bee и три строки кода в классе Queen. Класс EggCare и все остальные субклассы Bee будут автоматически реализовать IWorker, по- тому что они наследуют от класса, реализующего IWorker. Мы добавили в диаграм- му классов типы, моди- фикаторы доступа и все остальные подробности, чтобы предоставить чуть больше информа- ции о классах и интер- фейсе. Упражнение
412 глава 7 интерфейс добавлен в систему управления ульем Измените систему управления ульем так, чтобы для обращений к рабочим в ней использовал- ся интерфейс IWorker вместо абстрактного класса Bee. Для этого пришлось добавить интер- фейс IWorker, а также изменить классы Bee и Queen. Значительных изменений в коде не по- требовалось — ведь использование интерфейсов не требует большого объема лишнего кода. Вы началис добавления интерфейса IWorker в проект interface IWorker { string Job { get; } void WorkTheNextShift(); } Затем вы изменилиBee для реализации интерфейса IWorker abstract class Bee : IWorker { /* Остальной код класса остается без изменений */ } И наконец, вы изменили класс Queen так, чтобы вместо ссылок наBee в нем использовались ссылки на IWorker class Queen : Bee { private IWorker[] workers = new IWorker[0]; private void AddWorker(IWorker worker) { if (unassignedWorkers >= 1) { unassignedWorkers--; Array.Resize(ref workers, workers.Length + 1); workers[workers.Length - 1] = worker; } } private string WorkerStatus(string job) { int count = 0; foreach (IWorker worker in workers) if (worker.Job == job) count++; string s = "s"; if(count==1)s=""; return $"{count} {job} bee{s}"; } /* Остальной код класса Queen остается без изменений */ } Попробуйте изменить WorkerStatus так, чтобы заменить IWorker в ци- кле foreach на Bee: foreach (Bee worker in workers) Запустите код — он нормально ра- ботает! Теперь попробуйте заменить его на NectarCollector. На этот раз вы получите исключение System. InvalidCastException. Как вы думае- те, почему это происходит? Любой класс может реализовать ЛЮБОЙ интерфейс при усло- вии, что он соблюдает гарантии по реализа- ции методов и свойств интерфейса. Упражнение Решение
дальше 4 413 приведение типов интерфейсов и is В: Когда я помещаю свойство в интерфейс, оно выглядит как автоматическое свойство. Означает ли это, что при реализации интерфейсов могут использоваться только автоматические свойства? О: Вовсе нет. Действительно, свойство в интерфейсе очень похоже на автоматическое свойство (как свойство Job в интер- фейсеIWorker на следующей странице), но они определенно не являются автоматическими свойствами. Свойство Job можно былобы реализовать так: public Job { get; private set; } Приватный set-метод необходим, потому что автоматическое свойствотребуетналичия какget-, т ак и s et-метода(дажееслиони являются приватными).Такжереализация моглабы выглядеть так: public Job { get { return "Egg Care"; } } Компилятор это полностью устроит. Также можно добавить set-метод — интерфейс требует наличия get-метода, но он не запрещает иметь set-метод.(Еслииспользоватьавтоматическое свойство для реализации, вы можете самостоятельно решить, долженset-методбыть приватным или открытым.) В: Развенестранно,что в моих интерфейсах нет модифи- каторов доступа? Почему не пометить методы и свойство открытыми? О:Модификаторыдоступане нужны, потому чтовсекомпоненты интерфейса поумолчанию являются открытыми. Допустим,у вас имеетс я инт ерфейс в ида: void Honk(); Объявлениеговорит,что класс должен содержать открытый void- метод с именем Hon k, но нич его не с ообщает о том, чт о э тот метод должен делать. Он можетделать все чтоугодно — что бы он ни делал, кодбудетнормально компилироваться приусловии, что в нем присутствует метод с подходящей сигнатурой. Выглядит знакомо?Правильно, потому что вы уже видели этот принцип —в абстрактных классах в главе6.Когдавы объявляете в интерфейсе методы или свойствабез тел, они автоматически становятся открытыми и абстрактными, как абстрактные ком- поненты, используемые в абстрактных классах. Они работают точно так же, как любые другие методы или свойства, — х от я ключевое слово abstract явно не включается, его присутствие подразумевается.Именнопоэтой причинекаждыйкласс,реализу- ющий интерфейс,долженреализовать каждый его компонент. Люди, занимавшиеся проектированиемC#, могли бы заставить вас помечать все компоненты как открытые и абстрактные, но это былобы излишне. Поэтому они сделали все компоненты от- крытыми и абстрактными по умолчанию, чтобы код программы был более понятным. Все компоненты открытого интерфейса автоматически являются открытыми, потому что они используются для определения открытых методов и свойств любого класса, реализующего этот и нтерфей с. часто Задав ае мые вопросы
414 глава 7 как отказаться от использования строки для задания Свойство Job в интерфейсе IWorker — костыль Всистемеуправленияульем свойствоWorker.Job используется по схеме: if (worker.Job == job). Вам здесь ничего не кажется странным?Нам кажется. Мы считаем, что это костыль — неэлегантное, примитивноерешение.ПочемуиспользованиеJobкажется нам костылем?Представьте, чтопроизойдет, есливы допуститеопечатку: class EggCare : Bee { public EggCare(Queen queen) : base("Egg Crae") // В классе EggCare появляется ошибка, хотя // остальной код класса остается неизменным. } Теперь кодне может определить,указывает лиссылкаWorkerна экземплярEggCare. Возникаеткрайне коварнаяошибка,которую очень трудно обнаружить. В этом коде определенно повышается риск оши- бок... Но почему мыназываем его «костылем»? Ранееговорилось оразделенииобязанностей:веськод, относящийсякрешениюконкретной задачи,дол- жен хранитьсявместе. СвойствоJob нарушаетпринципразделения обязанностей.Еслиувас имеетсяссылка на Worker, то вам уже не нужно проверять строку, чтобыузнать, указывает ссылка на объектEggCare или наобъектNectarCollector. СвойствоJob возвращает «EggCare» дляобъектаEggCare, «NectarCollector»для объектаNectarCollector ииспользуется только для проверки типа объекта. Но ведь эта информация уже отслеживается втипе объекта. Ко-стыль, сущ. В технике — уродливое, неуклю- жее или неэлегантное решение задачи, которое создает трудно- сти с сопровождением. Кажется, я понимаю, к чему вы клоните. Наверняка C# дает мне возможность проверить тип объекта без использования костылей, верно? Верно! C# предоставляет вам необходимые средства для работы с типами. Дляпроверки типовклассовне нужно добавлять лишние свойства (такие, какJob)истроки«EggCare» или «NectarCollector». C#предо- ставляет ввашераспоряжениесредствадляпроверкитипаобъекта. Мы совершили опечатку в «Egg Care» — согласитесь, такуюопе- чатку может сделать каждый. Вы представляете, насколько трудно будет найти подобную ошибку, вызванную простой опечаткой?
дальше 4 415 приведение типов интерфейсов и is Использование is для проверки типа объекта Какизбавиться от костыля со свойствомJob?Непосредственно сейчас объектQueen содержитмассив workers; это означает,что онможетполучить ссылку наIWorker. ПрипомощисвойстваJobQueen опре- деляет,какие рабочие относятся ккатегорииEggCare, а какие — ккатегорииNectarCollector: foreach (IWorker worker in workers) { if (worker.Job == "Egg Care") { WorkNightShift((EggCare)worker); } void WorkNightShift(EggCare worker) { // Код отработки смены } Как вы уже видели, если случайно ввести «Egg Crae» вместо «Egg Care», код терпит крах самым позорным образом. А если случайно задать свойствуJob объекта HoneyManufacturer значение «EggCare», вы получитеошиб- ку InvalidCastException. Было бы замечательно, если бы компилятормогвыявлять подобные проблемына стадиина- писаниякода —подобнотому, какмыиспользуемприватные иоткрытые компоненты дляобнаружениядругихпроблем. C# предоставляет специальное средство дляэтойцели:ключевоеслово is проверяет тип объекта. Если увасимеетсяссылканаобъект,воспользуйтесьisдляпроверкитого,относитсяли онак конкретномутипу: objectR efer ence is ObjectType newVariabl e Еслиобъект, на которыйуказывает objectReference, относитсяктипу ObjectType, то is возвращает true исоздается новаяссылкаэтого типа с именем newVariable. Такимобразом, если пчелиная матка хочет найти всех своихрабочих-опекунов EggCare и заставить их работать в ночную смену, она может воспользоваться ключевым словом is: foreach (IWorker worker in workers) { if (worker is EggCare eggCareWorker) { WorkNightShift(eggCareWo rker); } } Команда if вэтом циклеиспользуетisдляпроверки каждойссылкинаIWorker. Присмотритесь повнимательнеек проверкеусловия: worker is EggCare eggCareWorker Еслиобъект, на которыйссылаетсяпеременнаяworker,являетсяобъектомEggCare, условие возвращает true, а команда is присваивает ссылку новой переменнойEggCare с именем eggCareWorker. Происходя- щее напоминает приведение типов, но команда isбезопасно выполняет приведение за вас. Массив IWorker ? ? ? об ъ е к т ? ? ? объ е к т ? ? ? о б ъ ект Ключевое слово is возвращает true, если объект соот- ветствует задан- ному типу, и C# может объявить переменную со ссылкой на этот объект.
416 глава 7 проверка класса как типа Использование is для обращения к методам субкласса Объединим все, о чем говорилось ранее, в новом проекте. Мы создадим простую модель классов, на вершине которой находится Animal, затем идут классы Hippo и Canine, расширяющие Animal, и класс Wolf расширяет Canine. Создайтеновоеконсольное приложение идобавьте в него классыAnimal,Hippo, Canine иWolf: abstract class Animal { public abstract void MakeNoise(); } class Hippo : Animal { public override void MakeNoise() { Console.WriteLine("Grunt."); } public void Swim() { Console.WriteLine("Splash! I'm going for a swim!"); } } abstract class Canine : Animal { public bool BelongsToPack { get; protected set; } = false; } class Wolf : Canine { public Wolf(bool belongsToPack) { BelongsToPack = belongsToPack; } public override void MakeNoise() { if (BelongsToPack) Console.WriteLine("I'm in a pack." ); Console.WriteLine("Aroooooo!"); } public void HuntInPack() { if (BelongsToPack) Console.WriteLine("I 'm going hunting with my pack!"); el se Console.WriteLine("I'm not in a pack."); } } Animal abstract MakeNoise Hippo MakeNoise Swim Canine Be l on gsToP ack Wo lf MakeNoise HuntInPack Класс Wolf расширя- ет Canine и добавляет собственный метод HuntInPack. Метод HuntInPack присут- ствует только в классе Wolf. Он не наследуется от суперкласса. Абстрактный класс Animal на- ходится на вершине иерархии. Субкласс Hippo переопре- деляет абстрактный ме- тод MakeNoise и добавляет собственный метод Swim, который не имеет никакого отношения к классу Animal. Абстрактный класс Canine расширяет Animal. Он содер- жит свое абстрактное свой- ство BelongsToPack. Сделайте это!
дальше 4 417 приведение типов интерфейсов и is Заполним код метода Main. Он делает следующее: ‘ Онсоздает массивобъектовHippoиWolf, азатемперебирает все объекты вциклеforeach. ‘ Он использует ссылку на Animal для вызова метода MakeNoise. ‘ Если текущим объектом является Hippo, метод Main вызывает его метод Hippo.Swim. ‘ Еслитекущим объектом являетсяWolf, метод Main вызывает его метод Wolf.HuntInPack. Проблема в том, что если увас имеетсяссылканаAnimal,котораяуказыва- етнаобъектHippo, вынесможете использоватьеедлявызоваHippo.Swim: Animal animal = new Hippo(); animal.Swim(); // <-- Эта строка не компилируется! Неважно, чтовыработаете с объектомHippo.Есливыне используете переменнуюAnimal,то высможете обращаться только к полям,методам и свойствамAnimal. Ксчастью,упроблемы есть обходноерешение. Есливы на стопроцентовуверены втом,что работаете с объектомHippo, то вы можетепреобразовать ссылку наAnimalв ссылку на Hippo — ипослеэтого обратиться к методу Hippo.Swim: Hippo hippo = (Hippo)animal; hippo.Swim(); // Объект остался тем же, но теперь вы можете вызвать метод Hippo.Swim. А воткаквыглядит метод Main, в котором ключевое слово is используется для вызова Hippo.Swim или Wolf.HuntInPack: class Program { static void Main(string[] args) { Animal[] animals = { new Wolf(false), new Hippo(), new Wolf(true), new Wolf(false), new Hippo() }; foreach (Animal animal in animals) { animal.MakeNoise(); if (animal is Hippo hippo) { hippo.Swim(); } if (animal is Wolf wolf) { wolf.HuntInPack(); } Console.WriteLine(); } } } В главе 6 вы узнали, что разные ссылки могут использоваться длявызова ра зных методов одного объекта. Когда вы не использовали ключевые слова override и virtual, а ваша ссы- лочная переменная относилась к типу Locksmith, она вызывала метод Locksmith.ReturnContents; но когда переменная относилась к типу JewelThief, вызывался метод JewelThief.ReturnContents. Здесь происходит нечто похожее. Воспользуйтесь отладчиком и постарайтесь доско- нально разобраться в том, что же здесь происходит. Установите точку прерывания в первой строке цикла foreach; добавьте отслеживания для animal, hippo и wolf; выполните программу в пошаговом режиме. Цикд foreach перебирает элементы массива «animals». Ему нужно объявить переменную типа Animal, соответ- ствующего типу массива, но по этой ссылке нельзя будет обратиться к ме- тодам Hippo.Swim или Wolf.HuntInPack. Цикл foreach перебирает элементы массива «animals». Ему нужно объявить переменную типа Animal, соответству- ющего типу массива, но по этой ссылке нельзя будет обратиться к методам Hippo.Swim или Wolf.HuntInPack.
418 глава 7 реализация интерфейса для выполнения задания А если мы захотим, чтобы другие животные плавали или охотились в стае? А вы знаете, что львы тоже охотятся стаей (HuntInPack)? И что тигры умеютплавать(Swim)?Акакнасчетсобак,которыеохотятся стаейИпла- вают?Есливы попытаетесь добавить методы Swim и HuntInPack ко всем животным в нашем зоопарке, которым они могут понадобиться, цикл foreach будет становиться все длиннее и длиннее. Красотаопределенияабстрактного метода илисвойства вбазовомклассе и его переопределения в субклассе заключаетсяв том, что вам не нужно ничего знать о субклассе, чтобы использовать его. Можно добавить сколько угодно субклассовAnimal,и цикл все равнобудет работать: foreach (Animal animal in animals) { animal.MakeNoise(); } МетодMakeNoiseвсегда будетреализованобъектом. Собственно, этотфактможнорассматривать как контракт, соблюдениекоторого обеспечивается компилятором. Итак, если бы методы HuntInPack и Swim тоже могли рассматриваться как контракты, мы бы могли использовать с ними другие обобщенные переменные — подобно тому, как это делается с классом Animal? Fel ine Canine Animal abstract Make- Noise L ion MakeNoise HuntInPack Hippo MakeNoise S wim Tiger MakeNoise S wim Dog MakeNoise HuntInPack S wim Bobcat MakeNoise Wo lf MakeNoise HuntInPack Объекты Wolf и Dog едят и спят одина- ково, но из- дают разные звуки. BelongsToPack
дальше 4 419 приведение типов интерфейсов и is Использование интерфейсов для работы с классами, выполняющими одну задачу Классы плавающих животных содержатметод Swim, а классы животных, охотящихся стаей, содержат метод HuntInPack. Допустим, это неплохое начало. Теперь мы хотим написать код, работающий с объ- ектами,которыеплаваютилиохотятся стаей, — и здесь по-настоящему проявляетсямощь интерфейсов. Воспользуйтесь ключевым словом interface для определения двух интерфейсов и добавьте абстракт- ный компонент вкаждый интерфейс: interface IS wimmer { void Swi m(); } interface IP ackHunter { void Hun tInPack(); } ЗатемзаставьтеклассыHippo иWolfреализоватьинтерфейсы,добавив интерфейсв конец объявления каждого класса.Используйтедвоеточие(:)дляобозначенияреализацииинтерфейса,поаналогиистем, какделается прирасширении класса. Если классужерасширяетдругойкласс, достаточно поставить за- пятую послеимени суперкласса иуказать имя. Послеэтого необходимо проследить за тем,чтобы класс реализовал все компоненты интерфейса, иначе компилятор выдаст сообщение об ошибке. class Hippo : Animal, ISwimmer { /* Код остается неизменным — и он ДОЛЖЕН включать метод Swim. */ } class Wolf : Canine, IPackHunter { /* Код остается неизменным — и он ДОЛЖЕН включать метод HuntInPack. */ } Используйте ключевое слово «is» для проверки того, что животное плавает или охотится стаей Припомощи ключевого слова is можно проверить, реализует ликонкретныйобъект заданный интер- фейс; этот способработаетнезависимоот того, какие еще интерфейсыреализуютсяобъектом.Еслипере- меннаяanimalссылаетсянаобъект, реализующийинтерфейсISwimmer, топроверкаanimalis ISwimmer дает результат true и выможете безопасно привести его кссылке наISwimmerдля вызова метода Swim. foreach (Animal animal in animals) { animal.MakeNoise(); if (animal is ISwimmer swimmer) { swimmer.Swim(); } if (animal is IPackHunter hunter) { hunter.HuntInPack(); } Console.WriteLine(); } Добавьте! Как будет выглядеть ваш код, если у вас появятся 20 разных плавающих субклассов Animal? Понадобится20 разных команд if (animal is...) , которые будутпре- образовывать animal к разным суб- классам для вызова метода Swim. Прииспользовании ISwimmer достаточно одной проверки. Как и прежде, мы используем ключевое слово is, но на этот раз оно используется с интер- фейсами. При этом работает оно точно так же, как прежде.
420 глава 7 is предотвращает небезопасные преобразования is и безопасная навигация по иерархии классов Помните,как вы выполняли упражнение с заменойBee на IWorker в системеуправленияульем?Тогда ещевы столкнулись с исключениемInvalidCastException. Теперьмы объясним, почему это произошло. Ссылку на NectarCollector можно безопасно преобразовать в ссылку на IWorker. ВсеэкземплярыNectarCollector являются Bee(т. е. расширяютбазовый класс Bee), поэтому вы всегда можете воспользоваться оператором =, чтобы получить ссылку на NectarCollector и при- своить ее переменной Bee. HoneyManufact urer lily = n ew H oneyManufact urer(); Bee hiveMember = lily; А поскольку объектBeeреализует интерфейс IWorker, его тоже можно безопасно преобразовать к ссылке на IWorker. HoneyManufact urer daisy = new HoneyManufacturer(); IWorker worker = daisy; Преобразования такого рода безопасны: они никогда не приводят к выдаче исключения IllegalCastException,потомучтоонитолькоприсваиваютболееконкретные объекты переменным болееобщего типа в тойжеиерархии классов. Ссылку на Bee невозможно безопасно преобразовать в ссылку на NectarCollector. Безопасное преобразованиевозможнов другомнаправлении (преобразование Bee вNectarCollector), потомучтоневсеобъектыBeeявляютсяэкземплярамиNectarCollector.Например,HoneyManufacturer определенно не является NectarCollector. Таким образом, следующее преобразование: IWorker pearl = new HoneyManufacturer(); Necta rCollect or i rene = (Necta rCollect or)pearl; является недействительным: вы пытаетесь преобразоватьобъект к переменной, которая не соответствуетего типу. Ключевое словоis позволяет безопасно выполнять преобразования типов. Ксчастью,ключевоеслово isбезопаснееприведения типовскруглымискобками. Оно позво- ляет проверить, что тип соответствуетзаданному ивыполняет приведение к новой переменной только присоответствии типов. if (pearl is NectarCollector irene) { /* Код , ис польз ующий об ъект NectarC ollector. */ } Этоткодникогданевыдает исключениеInvalidCastException,потомучтокод,использующийобъ- ектNectarCollector,выполняетсятольковтомслучае, еслиpearl являетсяобъектомNectarCollector.
дальше 4 421 приведение типов интерфейсов и is В массиве слева используются типы измодели классов Bee.Два из этих типов не ком- пилируются—вычеркнитеих.Справа приведены трикомандыс использованием клю- чевого словаis.Запишите, прикаких значениях i каждая изних даст результат true. IWorker[] bees = new IWorker[8]; bees[0] = new HiveDefender(); bees[1] = new NectarCollector(); bees[2] = bees[0] as IWorker; bees[3] = bees[1] as NectarCollector; bees[4] = IDefender; bees[5] = bees[0]; bees[6] = bees[0] as Object; bees[7] = new IWorker(); 1. (bees[i] is IDefender) 2. (bees[i] is IWorker) 3. (bees[i] is Bee) В C# также существует другой инструмент для безопасного преобразования типов: ключевое слово as C#предоставляет ещеодининструментдля безопасных преобразований: ключевое слово as. Также as выполняет безопасные преобразованиятипов.Вот каконоработает:предположим, увас имеетсяссылка на IWorker с именем pear1 и вы хотитебезопасно преобразовать ее впеременную NectarCollector с име- нем irene. Безопасное преобразованиев NectarCollector может быть выполнено следующим образом: NectarCollector irene = pearl a s NectarCollecto r; Еслитипысовместимы,то эта команда присваиваетпеременнойireneссылкуна тотжеобъект, на кото- рыйуказывает переменнаяpearl.Еслитип объекта несоответствуеттипу переменной, то исключение невыдается.Вместо этого переменнойприсваивается null, что можно проверить командой if: if (irene != nu ll) { /* Код, в кот ором использует ся объект Nectar Collector */ } В очень старых версиях C# ключевое слово is работает иначе. Ключевое слово is существует в C# в течение долгого времени, но толь- ко в версии C# 7.0 , выпущенной в 2017 году, is позволяет объявить новую переменную. Таким образом, если вы работаете в Visual Studio 2015, следующая конструкция будет вам недоступна: if (pearl is NectarCollector irene) {...} . Вместо этого придется использовать ключевое слово as для выполнения преобразований, а затем проверить результат и определить, равен ли он null: NectarCollector irene = pearl as NectarCollector; if (irene != null) { /* Код, в котором используется ссылка irene */ } Будьте осторожны! Возьми в руку карандаш
422 глава 7 перемещение по иерархии классов Использование повышающего и понижающего приведения типа для перемещения вверх и вниз по иерархии классов На диаграммах классовбазовыйкласс обычноразмещается наверху,подним идут субклассы, ниже рас- полагаютсясубклассысубклассов,ит.д. Чем выше класс надиаграмме, темболее абстрактна егоприрода; чемнижекласс надиаграмме,темонконкретнее. «Абстрактноенаверху,конкретноевнизу» неявляется непреложнымправилом; этосоглашение, благодарякоторомувы можетес первоговзгляда определить, как работают наши моделиклассов. Вглаве6мы говорилио том,что субкласс всегда можетиспользоваться вместо базового класса,откото- рогооннаследует,но базовый класс далеко невсегда можетиспользоваться вместо расширяющего его субкласса. На это правило также можно взглянуть иначе: в каком-то смысле вы перемещаетесь вверх и вниз по иерархии классов. Допустим, вы начинаете со следующейкоманды: NectarCollector ida = new NectarCollector(); Оператор =можетиспользоваться для нормального присваивания(для суперклассов)илиприведения типа (для интерфейсов). Эти операции означают перемещение вверх по иерархии и называются повы- шающим приведением типа (upcasting): // Выполнить повышающее преобразование NectarCollector в Bee Bee beeReference = ida; // Это повышающее преобразование безопасно, потому что все объекты Bee реализуют IWorker IWorker worker = (IWorker)beeReference; Такжеможноперемещатьсяпоиерархиивдругомнаправлении, используя операторisдлябезопасного перемещения вниз по иерархии классов. Такое преобразование называется понижающим приведением типа (downcasting). // Выполнить понижающее преобразование IWorker в NectarCollector if (worker is NectarCollector rose) { /* Код, использующий ссылку rose */ } повышающее приведение типа Использует обычное присваивание или приведение типа для подъема по иерархии классов понижающее приведение типа Испол ьзу ет is для бе зопа с ного с пуска по иерархии классов NectarCollector overridefloatCostPerShift protected overrideDoJob HoneyManufacturer overridefloatCostPerShift protectedoverrideDoJob IWor k er stringJob voidWorkTheNextShift Bee stringJob virtualfloatCostPerShift (read-only) voidWorkTheNextShift protectedvirtualDoJob
дальше 4 423 приведение типов интерфейсов и is Пример повышающего приведения типа Есливыпытаетесьвычислить,какбывам сэкономить на счетахза электриче- ство,вас на самом деле не интересует, что делает каждый из ваших электро- приборов, — важно лишь то, что они потребляют энергию. Таким образом, есливы пишетепрограмму дляотслеживанияпотребления электричества,то скорее всего, вы ограничитесь написаниемклассаAppliance(Электроприбор). Но есливам нужно отличать кофеварку от печи, вы построите иерархию классов и добавите методы и свойства, специфические для кофеварки или печи,вклассы CoffeeMakerиOven;этиклассы наследуют от классаAppliance, которыйсодержитихобщие методы и свойства. Затем пишется метод для отслеживания энергопотребления: void MonitorPow er(Appliance app liance) { /* код, доба вляющий информац ию в базу данных энергопотребления домашних электроприборов */ } Чтобывоспользоватьсяэтим методом для отслеживания энергопотребления кофеварки, вы создаете экземпляр CoffeeMaker и передаете ссылку на него неп ос р едс твенно м етоду : CoffeeMaker mr Coffee = new Cof feeMaker(); MonitorPower(misterCoffee); CoffeeMaker CoffeeLeft FillWithWater StartBrewing Appliance ConsumePower Oven Capacity Preheat CookFood Отличный пример повышающего преобразования. Хотя метод MonitorPower получает ссылку на объ- ект Appliance, ему также можно передать ссылку mrCoffee, потому что CoffeeMaker является субклас- сом Appliance. В массиве слеваиспользуютсятипыиз модели классов Bee. Два из этихтипов не компилируются — вычеркните их. Справа приведены три команды с использо- ванием ключевого слова is. Запишите, при каких значениях iкаждая из них даст результатtrue. IWorker[] bees = new IWorker[8]; bees[0] = new HiveDefender(); bees[1] = new NectarCollector(); bees[2] = bees[0] as IWorker; bees[3] = bees[1] as NectarCollector; bees[4] = IDefender; bees[5] = bees[0]; bees[6] = bees[0] as Object; bees[7] = new IWorker(); 1 . (be es[i] is I Defen der) 2. (b ees[i ] is IWork er) 3. (bees[i] is Bee) 0,2и6 0,1,2,3,5,6 0,1,2,3,5,6 Эта стро- ка преобра- зует IWorker в NectarCollector, а зате м сно- ва сохраняет его по ссылке IWorker. Элементы 0, 2 и 6 в массиве указ ыв ают на один и тот же объект HiveDefender. Все эти объ- екты рас- ширяют Bee, а Bee реали- зует IWorker, так что все они являются объектами Bee и IWorker. Возьми в руку карандаш Решение
424 глава 7 другие примеры повышающего и понижающего приведения типа Повышающее приведение преобразует CoffeeMaker в Appliance Когда вы заменяетебазовый класссубклассом, например используетеCoffeeMakerвместоApplianceили HippoвместоAnimal,это называетсяповышающимприведением типа. Это чрезвычайно мощныйин- струмент, применяемый припостроении иерархий классов. Единственный недостаток повышающего приведениязаключается втом, что вы можетеиспользовать только свойства иметодыбазового класса. Иначеговоря,когда вы рассматриваетеCoffeeMakerкакAppliance,вынесможетеприказать кофеварке сварить кофеилизаполнить ее водой.Вы можете проверить,включенприбор или нет, потомучто это можно сделать с любым объектом Appliance (а свойство PluggedIn является частью класса Appliance). Создайте несколько объектов. Начнем c создания нескольких экземпляров CoffeeMaker и Oven: CoffeeMaker misterCoffee = new CoffeeMaker(); Oven oldToasty = new Oven(); Как создать массив объектов Appliance? Вы несможете поместить CoffeeMaker в массив Oven[], как и поместить Oven в массив CoffeeMaker[].Приэтом оба вида объектов можно раз- местить в массивеAppliance[]: Appliance[] kitchenWare = new Appliance[2]; kitchenWare[0] = misterCoffee; kitchenWare[1] = oldToasty; Однако вы не можете рассматривать любой экземпляр Appliance как Oven. Еслиу васимеется ссылканаAppliance,высможетепоней обращаться толькок методамисвойствам, связаннымсэлектроприборамивообще.Вынесможете использовать методыисвойстваCoffeeMaker поссылкеAppliance, даже есливыточнознаете, что имеете делосCoffeeMaker.Такимобразом, следую- щиекомандыработаютнормально,потому чтоониработаютсобъектомCoffeeMakerкаксAppliance: Appliance powerConsumer = new CoffeeMaker(); powerConsumer.ConsumePower(); Ноприпопытке использовать объекткакCoffeeMaker: powerConsumer.StartBrewing(); код компилироваться небудети вIDE будет выведено сообщение об ошибке: Послеповышающего приведения от субкласса к базовому классу вамбудут до- ступнытолькометодыисвойства,соответствующиессылке,используемойдля обращения к объекту. 1 2 3 О б ъ е к тC o f f e e M a k e r Эта строка не компилируется, потому что powerConsumer явля- ется ссылкой на Appliance и мо- жет использоваться только для выполнения операций Appliance. Вы можете воспользоваться повышающим при- ведением типа для создания массива с элемен- тами Appliance, в котором могут храниться экземпляры как CoffeeMaker, так и Oven. powerConsumer представляет собой ссыл- ку на Appliance, указывающую на объект CoffeeMaker. p o w e r c o n s u m e r Включать этот код в приложение необязательно — просто прочитайте его и постарай- тесь понять, как работают повы- шающие и по- нижающие при- ведения типа. Вы еще неоднократно п отр ени руетесь в их применении.
дальше 4 425 приведение типов интерфейсов и is Понижающее приведение преобразует Appliance в CoffeeMaker Повышающее приведение типа — замечательный инструмент, потому что онпозволяет использовать CoffeeMakerилиOven везде, где нужен объектAppliance.Впрочем,у негоестьбольшойнедостаток: если выиспользуете ссылкунаAppliance, котораяуказывает на объектCoffeeMaker, высможетеиспользовать только методыисвойства, принадлежащие Appliance.В такихситуацияхна помощь приходит понижаю- щееприведение типа:вы беретеранееповышеннуюссылку иизменяете ее обратно.Чтобыпроверить, что Appliance в действительности является объектом CoffeeMaker и его можно преобразовать обратно вCoffeeMaker,используйтеключевое словоis. Начнем со ссылки CoffeeMaker, уже подвергнутой повышающему приведению типа. Для этого использовался следующий код: Appliance powerConsumer = new CoffeeMaker(); powerConsumer.ConsumePower(); А если потребуется преобразоватьAppliance обратно в CoffeeMaker? Допустим,вы строитеприложение,котороеобращается кмассивуссылок наAppliance,чтобы объектCoffeeMaker могначатьварить кофе.Выне можетевоспользоватьсяссылкой наAppliance для вызова метода CoffeeMaker: Appliance someAppliance = appliances[5]; someAppliance.StartBrewing() Эта команда не будет компилироваться — вы получите ошибку компиляции«’Appliance’ несо- держит определения ‘StartBrewing’», потому чтоStartBrewing является компонентомCoffeeMaker, а выиспользуетессылкуна Appliance. О б ъ е к тC o f f e e m a k e r Ссылка на Appliance, кото- рая указывает на объект CoffeeMaker. Она может использоваться только для обращения к компонентам класса Appliance. p o w e r c o n s u m e r Но так как мы точно знаем, что имеем дело с объектом CoffeeMaker, мы работаем с объектом соответствующим образом. Первым шагом становитсяключевоесловоis.Когда вы точно знаете,что ссылка Applianceука- зываетна объектCoffeeMaker,вы можетевоспользоватьсяis для еепонижающего приведения. Этопозволитвамиспользоватьметодыи свойства классаCoffeeMaker. ТаккакклассCoffeeMaker наследует отAppliance, он содержит все методы и свойства Appliance. if (someAppliance is CoffeeMaker javaJoe) { javaJoe.StartBrewing(); } О б ъ е к тC o f f e e M a k e r Ссылка javaJoe указывает на тот же объ ект CoffeeMaker, что и powerConsumer, но яв- ляется ссылкой на CoffeeMaker и может использоваться для вызова метода StartBrewing. s o m e A p p l i a n c e java J o e 1 2 3
426 глава 7 приведения типов с интерфейсами Повышающие и понижающие приведения типов также работают и с интерфейсами Интерфейсы отлично работают с повышающими и понижающими приведе- ниямитипов. Добавим интерфейсICooksFood для каждого класса, который умеетразогревать еду. Затем добавим классMicrowave — иMicrowave, и Oven реализуют интерфейсICooksFood.Теперь ссылкой наобъектOven может быть ссылка на ICooksFood, ссылка наMicrowave илиссылка на Oven. Этоозначает, чтовыможетесоздатьссылкитрехразныхтипов,указывающие наобъектOven, икаждаяизнихможет использоватьсядляобращениякразнымкомпонентам взависимостиот типа ссылки. Ксчастью, функцияIntelliSense вIDEпоможет точно определить, что можно илинельзяделать с каждой из них: Oven misterToasty = new Oven(); misterToasty. Чтобы обратиться к компонентам интерфейса ICooksFood, преобразуйте ссылкув ссылку наICooksFood: if (misterToasty is ICooksFood cooker) { cooker. ЭтототжеклассOven,который использовалсяранее, поэтомуон такжерасши- ряетбазовый классAppliance.Есливыможете использоватьссылкунаAppliance для обращения к объекту, видны будут только компоненты класса Appliance: if (misterToasty is Appliance powerConsumer) powerConsumer. ICooksFood Capacity CookFood Ov en Capacity Temperature Preheat CookFood Broil Microwave Capacity PowerLevel CookFood Defrost MakePopcorn Любой класс, реали- зующий ICooksFood, представляет элек- троприбор, способный разогревать еду. Три разные ссылки, указывающие на один объект, могут обращаться к разным методам и свойствам в зависимости от типа ссылк и. Как только вы введете точ- ку, на экране появляется окно IntelliSense со списком всехком- понентов, которые вы можете использовать. misterToasty — ссылка на Oven, указывающая на об ъек т Oven; соот ветст вен но , по ней можно обращаться ко всем методам и свойствам. Это самый конкретный тип, который может указывать только на объ- екты Oven. cooker — ссылка на ICooksFood, указывающая на тот же объект Oven. По ней так- же можно обращаться только к компонентам ICooksFood, но ссылка также может указывать на объект Microwave. powerConsumer является ссылкой на Appliance. По такой ссылке можно обращаться только к открытым полям, методам и свойствам вAppliance. Эта ссылка является более общей, чем ссылка на Oven (так что при желании она также может указывать на объектCoffeeMaker). По ссылке на Oven можно обращать- ся ко всем компонентам Oven. По ссылке на ICooksFood можно обра- щаться только к компонентам, которые явля- ются частью интерфейса. Appliance содержит только один компо- нент ConsumePower, и только он при- сутствует в рас- крывающемся списке.
дальше 4 427 приведение типов интерфейсов и is В:Еще раз: вы говорите, что повышающее при- ведение типа возможно всегда, но понижающее при- ведение возможно не всегда. Почему? О:Потому, что повышающее приведение не сработает, если вы попытаетесь назначить объекту класс, от которого он не насл еду ет, или инт ерфейс , котор ый он не реализ ует . Компилятор немедленно определяет, что повышающее приведение выполняется некорректно, и выдает ошибку. К ог да мы г оворим «по вышаю щее приве дени е воз можн о всегда, но понижающее приведение возможно не всегда», фактически мыговорим: «Каждая печь является электро- прибором, но не каждыйэлектроприбор является печью». В:Ячиталвинтернете, что интерфейсы определяют контракты, но не понимаю почему.Что это значит? О:Да,многиелюди сравнивают интерфейсы с контракта- ми. («В чеминтерфейс похож на контракт?»— этотвопрос очень часто задается на собеседованиях при приеме на работу.) Когда вы указываете, что ваш класс реализует интерфейс, вы тем самым обещаете компилятору, что он будет содержать определенные методы. Компилятор следит за тем, чтобы вы сдержали свое обещание. Если такаяточка зренияпоможет вампонять интерфейсы — по- жалуйста, рассматривайте их с этой точки зрения. Но на наш взгляд, лучше представить себе интерфейс как некий контрольный список. Компилятор проходит по списку иубеждается в том, что вы включили все методы интерфейса в ваш класс. Если вы этого не сделали, то компилятор протестует и программа компилироваться не будет. В:Зачем мне использовать интерфейсы? Такое впечатление, что мы просто добавляем новые огра- ничения, а класс при этом никак не изменяется. О:Потому что когда ваш класс реализует интерфейс, вы можете использовать этот интерфейс как тип для объявления ссылки, которая можетуказывать на любой экземпляр класса,реализующего этот интерфейс. Данная возможность очень полезна —онапозволяет создать один ссылочный тип, который может работать с множеством ра зноо браз ных о бъ ект ов. Рассмотрим небольшой пример.Лошадь, вол, мул ибык могут тянуть повозку.В нашей моделизоопаркаHorse,Ox, MuleиSteerбудутразными классами. Допустим,в вашем зоопарке имеется аттракцион, на котором посетители катаются на повозках, и вы хотите создать массив любых животных, способных тянуть повозку. И тут выясняется, что создать массив, в котором могут храниться все эти классы, невозможно . Это было бы возможно, если бы они наследовали от одного базового класса, но это не так. Что жеделать? На помощь приходят интерфейсы. Вы можете создать инт ерфе йс IPul le r с мет одами для п еремещ ения по возок . Тогда массив объявляется следующим образом: IPuller[] pullerArray; Теперь вмассив можно поместить ссылку на любое живот- ное — при условии, что оно реализует интерфейс IPuller. Интерфейс напоминает контрольный список, по которому компилятор проверяет, что ваш класс реализует необходимый набор методов. часто Задава ем ые вопросы
428 глава 7 интерфейсы и наследование Интерфейсы могут наследовать от других интерфейсов Какупоминалосьранее,одинкласс,наследующийотдругого,получаетвсеметодыи свойствабазового класса.Наследованиеинтерфейсовустроенопроще. Так какинтерфейс несодержитреальныхтел ме- тодов, вамне нужнобеспокоиться о вызове конструкторов или методовбазового класса. Наследующие интерфейсыпросто накапливаютвсе компонентырасширяемых интерфейсов. Как это выглядит в коде? Добавим интерфейс IDefender, наследующий от IWorker: interface IDefender : IWorker { void DefendHive(); } Есликлассреализуетинтерфейс,он должен реализовать каждое свойство икаждый методэтого интер- фейса.Если интерфейс наследует от другогоинтерфейса,то также должныбытьреализованы все свойства иметодыэтогоинтерфейса.Таким образом,любой класс,реализующийIDefender,долженреализовать не тольковсе компонентыIDefender,но ивсе компоненты IWorker. Следующаямодель классоввключает IWorkerи IDefender,а такжедве отдельныеиерархии, вкоторых ониреализуются. Интерфейс IDefender расш и ряет IW or ker, поэтому любой класс, реализующий его, дол- жен реализовать все компоненты интерфей- сов IWorker и IDefender. IWorker interface stringJob void WorkTheNextShift Bee stringJob virtualfloatCostPerShift (read-only) W ork TheN extS hi ft pr otected v i rtual DoJob IDefender interface DefendHi ve Robot Cons umeG as RoboBee W ork TheNex tS hift RoboDefender реализует IDefender, а его суперкласс RoboBee реализует IWorker, поэтому к нему можно об- ращ ат ься как ч ерез м ассив IWorker, так и через массив I Def en der. HiveDefender overridefloatCostPerShift pr otected ov err ide D oJob Defend Hiv e RoboDefender DefendHi v e HiveDefender расширяет Bee, поэтому он автома- тически реализует IWorker, так как Bee реализует этот ин- терфейс. Это озна- чает, что он может включать компо- ненты IWorker (или просто наследовать их от Bee), но дол- жен включать ком- поненты IDefender, пот ому что обяз ан реализовать все компоненты всех интерфейсов, кото- рые он реализует. Используйте двоеточие(:), чтобы обозначить, что интерфейс расширяет другой интерфейс.
приведение типов интерфейсов и is Создайте новое консольное приложение с классами, реализующими интерфейс IClown. Смо- жете ли вы разобраться в том, как должен строиться код нижних уровней? Начните ссоздания интерфейса IClown, созданного ранее: interface IClown { string FunnyThingIHave { get; } void Honk(); } Расширьте новый интерфейс IScaryClown, расширяющий IClown. Он долженсодержать строковое свойство с именем ScaryThingHave сget- методом, но без set-метода и void-метод с именем ScareLittleChildren. Создайте классы, реализующие эти интерфейсы: ‘ Класс с именем FunnyFunny, реализующий IClown. Он исполь- зует приватную строковую переменную с именем funnyThingHave. Get-метод FunnyThingHave использует funnyThingHave в качестве резервного поля. Используйте конструктор, который получает параметр и использует его для инициализации приватного поля. Метод Honk выводит сообщение: «Hi kids! I have a», значение funnyThingIHave и точку. ‘ Класс с именем ScaryScary, реализующий IScaryClown. Он исполь- зует приватную переменную с именем scaryThingCount для хране- ния целого числа. Конструктор задает как поле scaryThingCount, так и funnyThingIHave, наследуемое ScaryScary от FunnyFunny. Get-метод ScaryThingIHave возвращает строку с числом из конструктора, за ко- торым следует слово «spiders». Метод ScareLittleChildren выводит на консоль сообщение «Boo! Gotcha! Look at my...!» (... заменяется соот- ветствующим значением поля.) Ниже приведен новый код метода Main — к сожалению , он не работает. Сможете ли вы определить, как исправить его, чтобы он строил и выводил сообщения на консоль? static void Main(string[] args) { IClown fingersTheClown = new ScaryScary("big red nose", 14); fingersTheClown.Honk(); IScaryClown iScaryClownReference = fingersTheClown; iScaryClownReference.ScareLittleChildren(); } Прежде чем запускать этот код, запишите результат, который метод Main выведет на консоль (после исправления): Затем выполните код и проверьте свой ответ. 1 2 3 4 Лучше бы тебе не ошибаться... Ането... IClown FunnyThingIHave Honk ScaryScary ScaryThingIHave ScareLittleChildren FunnyFunny FunnyThingIHave Honk IScaryClown ScaryThingIHave ScareLittleChildren Клоун Fingers пугает. Упражнение
430 глава 7 нет! хватит страшных клоунов! Создайте новое консольное приложение с классами, реализующими интерфейс IClown. Смо- жете ли вы разобраться в том, как должен строиться код нижних уровней? Интерфейс IScaryClown расширяет IClown, добавляя свойство и метод: interface IScaryClown : IClown { string ScaryThingIHave { get; } void ScareLittleChildren(); } Класс FunnyFunny реализует интерфейс IClown и использует конструктор для инициализации резервного поля: class FunnyFunny : IClown { private string funnyThingIHave; public string FunnyThingIHave { get { return funnyThingIHave; } } public FunnyFunny(string funnyThingIHave) { this.funnyThingIHave = funnyThingIHave; } public void Honk() { Console.WriteLine($"Hi kids! I have a {funnyThingIHave}." ); } } Класс ScaryScaryрасширяет класс FunnyFunny и реализует интерфейс IScaryClown. Его конструктор использует ключевое слово base для вызова конструктора FunnyFunny, инициализирующего приватное резервное поле: class ScaryScary : FunnyFunny, IScaryClown { private int scaryThingCount; public ScaryScary(string funnyThing, int scaryThingCount) : base(funnyThing) { this.scaryThingCount = scaryThingCount; } public string ScaryThingIHave { get { return $"{scaryThingCount} spiders"; } } public void ScareLittleChildren() { Console.WriteLine($"Boo! Gotcha! Look at my {ScaryThingIHave}!"); } } Чтобы исправить метод Main, замените строки3 и 4 метода следующими строками, использующими оператор is: if (fingersTheClown is IScaryClown iScaryClownReference) { iScaryClownReference.ScareLittleChildren(); } FunnyFunny.funnyThingIHave — приватное поле, и класс ScaryScary не может обратиться к нему — он должен использовать ключевое словоbase для вызова конструктора FunnyFunny. Вы можете присвоить ссылке на FunnyFunny объектScaryScary, потому что ScaryScary наследуетотFunnyFunny.Ссыл- ке на IScaryClownневозможно присвоить произвольный объект клоуна, потому что вы не знаете, является ли этот клоун страш- ным (Scary). Именно по этой причине необ- ходимо использовать ключевое словоis. Интерфейс IScaryClown на- следует отинтерфейсаIClown. Это означает, что любой класс, реализующий IScaryClown, дол- жен содержать не только свой- ство ScaryThingIHaveи метод ScareLittleChildren, но и свойство FunnyThingIHave и метод Honk. Точ но та- кие же кон- структоры и резервные поля, как ис- пользовались в главе 5. Упражнение Решение
дальше 4 431 приведение типов интерфейсов и is Ключевое слово readonly Важная причина для использования инкапсуляции — пре- дотвращение случайной перезаписи данных одного клас- са со стороны другого класса. Что помешает классу перезаписать свои собственные данные? В этом может помочь ключевое слово readonly. Любое поле с пометкой readonly может быть изменено только при объявлении или в конструкторе. Безусловно! Ограничение доступа к полям только для чтения с пособс т вует пре дот вра ще нию ош ибок. Вернитесь к полю ScaryScary.scaryThingCount — IDE выводитточки подпер- вымидвумябуквами имени поля. Наведите указатель мышина точки, чтобы в IDE открылось окно: НажмитеCtrl+, чтобы вызвать список действий, и выберите команду «Add readonlymodifier», чтобы добавить ключевоеслово readonly вобъявление: Теперь значениеполя можетбыть задано только при объявлении или вкон- структоре. Есливыпопытаетесь изменить его значение влюбойдругойточке метода,компилятор выдаст сообщениеобошибке: Ключевое слово readonly... еще один механизм, используемый C# для улуч- шениябезопасностиваших данных. Я заметил, что IDE часто спрашивает меня, хочу ли я сделать поле доступным только для чтения. Стоит ли это делать?
432 глава 7 расширение классов, реализация интерфейсов В:Зачем использовать интерфейс, если можно просто записать все необходимые методы прямо в классе? О:Прииспользованииинтерфейсов вытакже записываете методы в своем классе. Интер- фейсы позволяют группировать эти классы в соответствии с работой, которую они выпол- няют. Они помогают следить за тем, чтобы каждый класс, выполняющий определенную разновидность работы, делал это с исполь- зованием одних и тех же методов. При этом класс можетвыполнять работу так, каксчитает нужным, и из -за интерфейса вам не нужно беспокоиться о том, как он это будетделать. Пример: в системе могут присутствовать классы Truck (Грузовик) и Sailboat (Яхта), реализующиеинтерфейс ICarryPassenger. До- пустим, интерфейсICarryPassenger требует, чтобы любойкласс,реализующийэтот интер- ф ейс , содерж ал м етод Cons umeE nergy. Т огд а программаможетиспользовать оберазновид- ности классов для перевозкипассажиров,не- смотря наточто методConsumeEnergy клас- са Sailboat использует силу ветра, а метод класса Truck используетдизельное топливо. Теперь представим, что интерфейс ICarry- Passenger отсутствует. Тогда вам придется туго — нужно будет как-то сообщить про- грамме, какие транспортные средства могут перевозить пассажиров, а какие нет. Вам придется просматривать каждый класс, ко- торый может использоваться программой, и определять, присутствует ли в нем метод для перевозки пассажиров из одного места в другое. И тогда пришлось бы вызывать для транспортных средств, которые могут использоваться ввашей программе, тотметод, который был в них определен для перевозки пассажиров. Приотсутствии стандартного интерфейса методам могут быть присвоены самые разные имена, они могутбыть спрятаны в другихметодах. Каквидите,ситуация очень быстроусложняется. В: Зачем использовать свойства в интерфейсах? Почемуне ограни- читься полями? О: Хор оший воп рос . Инт ерф ейс опре де- ляет только механизм выполнения классом к онкрет ной разно в иднос ти раб оты . Сам по себеобъектомоннеявляется,поэтому вы не сможете создать его экземпляр и сохранить в нем информацию.Если вы добавите поле, которое является простым объявлением переменной, C# придется где-то хранить эти данные, ведь интерфейс этого неможет. Свойство — механизм , при котором нечто выглядит для других объектов как поле, но таккакв действительностиэтометод, никакие данные насамомделе не сохраняются. В:Чем ссылка на обычный объект от- личается от ссылки на интерфейс? О: Вы уже знаете, как работают обычные ссылки на объекты. Еслисоздать экземпляр Skateboardс именем vertBoard, а потом новую ссылку на него с именем halfPipeBoard, обе ссылки будут указывать на одно и то же. Но если Skateboard реализует интерфейс IStreetTricks и вы создадитессылку наинтер- фейс,указывающую наSkateboard,с именем streetBoard,то понейбудутдоступны только методы класса Skateboard, которые также входят в интерфейсIStreetTricks. Всетри ссылки вдействительностиуказыва- ют на один и тот же объект. Если вы обрати- тесь к объекту по ссылкеhalfPipeBoard или vertBoard, вы сможете обратиться к любому методу илисвойству объекта. Если же вы обратитесь кнемупо ссылкеstreetBoard, вам будут доступны только методы и свойства из интер фейс а. В: Изачемтогда использовать ссылки на интерфейсы, если они только ограни- чивают возможности работы собъектом? О: Ссылки на интерфейсы позволяют ра- ботать с группами разнородных объектов, которые выполняют одну операцию. Вы можете создать массив с типами ссылок на интерфейсы, которые позволяют переда- вать информацию методамICarryPassenger и обратно независимо от того, работаетевы с объектом Truck, Horse, Unicycle или Car. Вероятно, каждый из этих объектов будет решатьсвоюзадачу не так,как другие,но со ссылками на интерфейсывы точно знаете, что они содержат одни и те же методы, которые получают одни и те же параметры и имеют те же возвращаемые типы. Итак, вы можете обращат ьс я к ним и п ередав ат ь информацию абс олю тно одинак ов о. В: Напомните, для чего мне стоит объ- явить компонент класса защищенным (protected) вместо приватного (private) или открытого (public)? О: Потому что это помогает улучшить инкапсуляцию классов. Во многих случаях субклассу необходим доступ к некоторой внутренней части своего базового класса. Например, если вы хотите переопределить свойство, достаточно часто в get-методе ис- пользуетсярезервное полебазового класса. При построении классов объявляйте ком- поненты открытыми, только если для этого есть вескиепричины. Модификатордоступа protected позволяет раскрыть компонент только для субкласса, которому он необ- ходим, и оставить его приватным для всех остальных. Ссылкам на интерфейсы известны только методы и свойства, определенные в интерфейсе. часто Зада вае мы е вопросы
дальше 4 433 приведение типов интерфейсов и is ¢ Интерфейс определяет методы и свойства, которые должны быть реализованы классом. ¢ Интерфейсы определяют обязательные компонен- ты в виде абстрактных методов и свойств. ¢ По умолчанию все компоненты интерфейсов являют- ся открытыми и абстрактными (поэтому ключевые слова public и abstract для компонентов не ука- зываются). ¢ Если вы указываете, что класс реализует интер- фейс при помощи двоеточия (:), класс должен ре- ализовать все компоненты интерфейса; в против- ном случае код не будет компилироваться. ¢ Класс может реализовать несколько интерфейсов (и проблема ромбовидного наследования не возник- нет, потому что интерфейсы не имеют реализации). ¢ Интерфейсы очень полезны, потому что они позволя- ют несвязанным классам выполнять одну задачу. ¢ Имена интерфейсов должны начинаться с буквы I верхнегорегистра (это всего лишь соглашение, компи - лятор не следит за его выполнением.) ¢ Отношения реализации интерфейсов обозначаются на диаграммах классов пунктирными стрелками. ¢ Экземпляр интерфейса невозможно создать ключе- вым словом new, потому что его компоненты явля- ются абстрактными. ¢ Интерфейс может использоваться как тип для ссылки на объект, который его реализует. ¢ Любой класс может реализовать любой интерфейс при условии, что он соблюдает обязательства по реа- лизации методов и свойств этого интерфейса. ¢ Все, что входит в открытый интерфейс, автоматиче- ски становится открытым, потому что интерфейс используется для определения открытых методов и свойств любого класса, который его реализует. ¢ Костыль — уродливое, неуклюжее или неэлегантное решение, которое создает трудности с сопровождением. ¢ Ключевое словоis возвращает true, если объект со- ответствует заданному типу. Также оно может исполь- зоваться для объявления переменной и присваивания ей по ссылке проверяемого объекта. ¢ Под повышающим приведением типа обычно пони- мается обычное присваивание или приведение типа с перемещение вверх по иерархии классов или при- сваивание переменной суперкласса ссылки на объект субк л асс а. ¢ Ключевое словоis позволяет выполнять понижающее приведение типа — безопасное перемещение вниз по иерархии классов — для использования переменной субклассадля обращения к объекту суперкласса. ¢ Повышающие и понижающие приведения типа также работают с интерфейсами — вы можете повысить ссылку на объект до ссылки на интерфейс или на- оборот. ¢ Ключевое слово as похоже на приведение типа, кроме того что при недействительности приведения типа оно возвращает null (вместо выдачи исключения). ¢ Если поле помечаетсяключевым словом readonly, его значение может быть задано только в инициализа- торе поля или в конструкторе. расши­ рить класс реализо­ вать интер­ фей с погла­ дить собаку Запомните, как работают интерфейсы: вы расширяете класс, но реализу- ете интерфейс. Когда речь идет о «расширении», обычно имеется в виду, что вы берете нечто существующее и добавляете к нему что-то новое (в данном случае — поведение). Под реализацией имеется в виду вопло- щение в жизнь соглашения — вы обязуетесь добавить все компоненты интерфейса (и компилятор следит за выполнением этого соглашения). Закрепляем в памяти КЛЮЧЕВЫЕ МОМЕНТЫ
434 глава 7 интерфейсы могут содержать команды Вообще-то вы можете добавить код в интерфейсы. Для этого включите в них статические компоненты и реализации по умолчанию. Возможности интерфейсов не сводятся к проверке того, что реа- лизующие их классы включают некоторые компоненты. Конечно, это их основная задача. Но интерфейсы также могут содержать код, каки любые другиеинструменты,используемыедля создания моделиклассов. Самый простой способ добавления кода в интерфейс основан на включениистатическихметодов, свойств иполей.Ониработают точно так же, как статические компоненты классов: в них могут храниться данные любого типа (включая ссылки на объекты) и их можно вызывать как любые другиестатическиеметоды: Interface. MethodName(); Также можно включить код в интерфейсы, добавляя в методы реа- лизациипо умолчанию.Чтобыдобавитьреализацию по умолчанию, просто добавьтетело метода винтерфейсе.Этотметоднеявляется частью объекта (этот механизм отличен от наследования!), и об- ратитьсякнему можно только по ссылке на интерфейс. Онможет вызывать методы, реализуемые объектом, приусловии, что они являются частью интерфейса. Я думаю, что у интерфейсов есть огромный недостаток. Когда я пишу абстрактный класс, я могу включить в него код. Не означает ли это, что абстрактные классы лучше интерфейсов? Реализации интерфейсов по умолчанию — от носит е льно но вая возм ожност ь C# . Если вы работаете в старой версии Visual Studio, возможно, вы не сможете использовать реализации по умолчанию, потому что они появились только в версииC# 8.0 , которая была опу- бликована вVisual Studio 2019 версии 16.3 .0 , выпущенной в сентябре 2019 года. Возможности текущей версии C# могут не поддерживать- ся в старых версиях Visual Studio. Будьте осторожны!
дальше 4 435 приведение типов интерфейсов и is Интерфейсы могут содержать статические компоненты Одиниз известных клоунских трюков — множество клоунов, втиснувшихся в маленький клоунский ав- томобиль!Обновим интерфейсIClownидобавим внего статическиеметоды,генерирующиеописание клоунского автомобиля. Вотчто для этого внего нужно добавить: ‘ Мыбудем использовать случайные числа, поэтому добавим статическую ссылку на экземплярRandom. Пока она применима только в IClown, но вскоре также будет использоваться в IScaryClown, поэтому ссылка будет помечена модификатором protected. ‘ Клоунскийавтомобильвыглядит смешнотольков томслучае, если онбитком набитклоунами,поэтомумыдобавим статическоесвойствоint сприватным статическим резервным полем иset-методом,который принимает только значениябольше10. ‘ Метод с именем ClownCarDescriptionвозвращает строку с описанием клоунского автомобиля. Нижеприведенкод — внем используетсястатическое поле,свойство и метод, какв обычном классе: interface IClown { string FunnyThingIHave { get; } void Honk(); protected static Random random = new Random(); private static int carCapacity = 12; public static int CarCapacity { get { return carCapacity; } set { if (value > 10) carCapacity = value; else Console.Error.WriteLine($"Warning: Car capacity {value} is too small"); } } public static string ClownCarDescription() { return $"A clown car with {random.Next(CarCapacity / 2, CarCapacity)} clowns"; } } Теперь можно обновить метод Main для обращения к статическим компонентам IClown: static void Main(string[] args) { IClown.CarCapacity = 18; Console.WriteLine(IClown.ClownCarDescription()); // Остальной код метода Main остается без изменений } Этистатическиекомпонентыинтерфейсов ведут себяточнотакже, какстатическиекомпоненты классов, встречавшиеся в предыдущих главах. Открытые компоненты могутиспользоваться из любого класса, приватные компоненты могут использоваться только изIClown, а защищенные компоненты могут ис- пользоваться из IClownилюбого интерфейса,которыйегорасширяет. Добавьте! IClown FunnyThingIHave staticCarCapacity protected staticRandom Honk staticClownCarDescription Статическое поле random помечено модификатором доступаprotected. Это означает, что к нему можно об- ращаться только из IClown или из любого интерфейса, расширяющего IClown (например, IScaryClown). Попробуйтедобавить приватное поле в свой интерфейс. Вы сможете его добавить — но только если оно является ста- тическим! Если убрать ключевое слово static, компилятор сообщит, что интерфейсы не содержат полей экземпляров.
436 глава 7 теперь ваши методы интерфейсов могут иметь тело Реализации по умолчанию определяют тело методов интерфейса Всеметоды,которыевстречались вам винтерфейсахдонастоящего времени,кроместатических,были абстрактными: онине имелитела, поэтому любойкласс, реализующийинтерфейс,должен предоставить реализацию этого метода. Новытакжеможетепредоставить реализациюпо умолчанию длялюбых методов своего интерфейса. Пример: interface IWorker { string Job { get; } void WorkTheNextShift(); void Buzz() { Console.WriteLine("Buzz!"); } } Выможетевызватьреализацию поумолчанию,нодлявызовадолжна использоваться ссылканаинтерфейс: IWorker worker = new NectarCollector(); worker.Buzz(); Этоткоднекомпилируется—выполучите сообщение обошибке «’NectarCollector’несодержитопределение‘Buzz’»: NectarCollector pearl = new NectarCollector(); pearl.Buzz(); Деловтом,что если метод интерфейса имеетреализацию по умолчанию,онстановится виртуальным методом (такимже,какметоды,которыевыиспользоваливсвоихклассах).Любойкласс,реализующий интерфейс,имеетвозможность реализовать метод. Виртуальныйметодсвязывается с интерфейсом.Как илюбаядругаяреализация интерфейса,онненаследуется.И этохорошо:еслибы класснаследовалреа- лизации по умолчанию отвсех интерфейсов,которыеонреализует, ивдвухтакихинтерфейсахприсут- ствовалиметодыс одинаковымиименами,вклассебы возниклапроблемаромбовидного наследования. Используйте @ для создания буквальных строковых литералов Символ @ имеет специальный смысл в программах C#. Если поме- стить его в начало строкового литерала, этот символ сообща- ет компилятору C#, что литерал должен интерпретироваться буквально. В частности, это означает, что символы \ не будут интерпретироваться как служебные последовательности, таким образом, строка @"\n" содержит обратную косую черту и символ n, а не символ новой строки. Кроме того, @ сообщает компиля- тору C# о включении всех разрывов строк. Таким образом, строка @"Line 1 Line 2" эквивалентна "Line1\nLine2" (включая разрыв строки.) Буквальные строко- вые литералы могут использоваться для создания «много- строчных» строк, включающих разрывы. Они нормально рабо- тают со строковой интерполяцией — просто добавьте $ в начало. При желании вы даже можете добавить в интерфейс при- ватные методы, но они будут вызывать- ся только из откры- тых реализаций по умолчанию.
дальше 4 437 приведение типов интерфейсов и is Добавление метода ScareAdults с реализацией по умолчанию НашинтерфейсIScaryClown идеально моделируетстрашных клоунов. Но тут возникает проблема: он содержиттолько метод, которыйпозволяетпугать детей.А если мыхотим,чтобы клоуны доводилидо полного ужаса ещеи взрослых? Дляэтого можно было бы включить абстрактный метод ScareAdults в интерфейс IScaryClown. Но что, еслиувасужеесть десятокклассов,реализующихIScaryClown? Ичто,еслибольшинствуиз нихпрекрас- ноподойдетодна реализацияметода ScareAdults?Именно втакихситуацияхреализацияпо умолчанию оказываетсяпо-настоящемуполезной.Реализация по умолчанию позволяет добавить методвуже исполь- зуемый интерфейсбез необходимостиобновления каких-либо классов,реализующих его.Добавьте в IScaryClown метод ScareAdults с реализацией по умолчанию: interface IScaryClown : IClown { string ScaryThingIHave { get; } void ScareLittleChildren(); void ScareAdults() { Console.WriteLine($@"I am an ancient evil that will haunt your dreams. Behold my terrifying necklace with {random.Next(4, 10)} of my last victim's fingers. Oh, also, before I forget..."); ScareLittleChildren(); } } Присмотритесь к тому, какработает метод ScareAdults. Он содержиттолько двекоманды, но вних упа- кована значительная функциональность. Проанализируем, что же здесь происходит: ‘ Команда Console.WriteLine использует буквальный литерал со строковой интерполяцией. Ли- терал начинается с символов$@, которые сообщают компиляторуC#два факта:$ приказывает использовать строковую интерполяцию, а @ — использовать форматбуквального литерала. Это означает,что строкабудетсодержать три внутреннихразрыва строк. ‘ Литералиспользует строковую интерполяциюдлявызоваrandom.Next(4,10), которыйиспользует приватное статическоеполеrandom,наследуемоеIScaryClown от IClown. ‘ Какупоминалосьранее, есликласс содержит статическое поле,это означает, чтосуществует толь- ко одна копия этого поля. Таким образом, существует только один экземпляр Random, который совместно используется какIClown, таки IScaryClown. ‘ В последней строке метода ScareAdults вызывается метод ScareLittleChildren. Этот метод яв- ляется абстрактным в интерфейсеIScaryClown, а следовательно, он будет вызывать версию ScareLittleChildren из класса, реализующего IScaryClown. ‘ Это означает, что ScareAdults будет вызывать версию ScareLittleChildren, определенную в классе, реализующем IScareClown. Вызовитеновуюреализацию по умолчанию, изменивблок после команды if в методе Main так, чтобы в нем вызывался метод ScareAdults вместо ScareLittleChildren: if (fingersTheClown is IScaryClown iScaryClownReference) { iScaryClownReference.ScareAdults(); } Добавьте! Здесь используется буквальный литерал. Также можно было использовать нор- мальный строковый литерал и добавить \n для разрывов строк. Такой синтаксис намного проще читается.
438 глава 7 интерфейсы в реальном мире Мне все время кажется, что интерфейсы — это какая-то теория. Я понимаю, как они работают, в маленьких примерах книги. Но используются ли они разработчиками в реальных проектах? Разработчики C# постоянно пользуются интерфейсами, особенно при работе с библиотеками, фреймворками и API. Разработчикивсегда «стоятна плечахгигантов». Выужеперешлико второй половинекниги, и в первой половине выписаликод, который выводит текст наконсоль,рисует окна скнопкамиистроит трехмер- ныеобъекты. Вам ненужнобыло писать код длявыводаконкретных байтов на консоль илирисования линий и текста для прорисовки кнопок вокне либо выполнять вычисления для вывода сферы — вы воспользовались кодом, написанным ранее другимилюдьми: ‘ Вы пользовались фреймворками(такими, как .NET Core и WPE). ‘ Вы пользовались API (например, API сценариев Unity). ‘ Фреймворки иAPIсодержатбиблиотекиклассов,ккоторым вы обращаетесь припомощи директив using вначале кода. Прииспользованиибиблиотек, фреймворковиAPIвы частоисполь- зуете интерфейсы. Убедитесь сами:откройтеприложение .NET Core илиWPF, щелкнитевнутрилюбогометода ивведите I, чтобывызвать окно IntelliSense. Любое потенциальное совпадение, помеченное значком , является интерфейсом. Всё это интерфейсы, которые могутиспользоватьсядля работысфреймворком. Небольшая часть ин- терфейсов, входящих в .NET Core.
дальше 4 439 приведение типов интерфейсов и is Связывание данных обеспечивает автоматическое обновление элементов WPF Рассмотрим отличныйпример использования интерфейсов для реальных целей: связывание данных. Связывание данных—исключительно полезныймеханизмWPF, которыйпозволяет настроить элементы управлениятак, что ихсвойствабудут автоматически задаватьсянаосновании значениясвойства объекта. Приизменении этого свойства происходит автоматическое изменение свойств элементовуправления. В системе управленияулья из главы 6 мы задавали значение statusReport. Text для обновления этого элемента TextBox. Мы изменим этот код так, чтобы в нем использовалось связывание данных, а поле TextBox обновлялось автоматически. Нижеприведен краткий обзор действий по изменению системы управленияульем — вскоре онибудут рассмотрены более подробно: Измените класс Queen и реализуйте интерфейс INotifyPropertyChanged. Этотинтерфейс позволяетQueen объявить, что отчет о текущем состоянии изменился. Измените код XAML, чтобы в нем создавался экземпляр Queen. Мысвяжем свойство TextBox.Text со свойством StatusReport класса Queen. Измените код программной части, чтобы в поле «queen» использовался только что созданный экземпляр Queen. Внастоящий моментполеqueen вфайлеMainWindow.xaml.cs использует инициализатор поля с командой new для создания экземпляраQueen. Мы изменим его так, чтобывместо этого ис- пользовалсяэкземпляр,созданный вXAML. 1 2 3 О б ъ е к т T ex t B o x О б ъ е кт Q u e e n СВЯЗЫВАНИЕ КОНТЕКСТ ДАННЫХ Свойство Text Свойство StatusReport INotifyPropertyChanged Связывание данных начинается с контекста данных — объекта, содержащего данныедля отображения в TextBox. Мы будем использовать экземпляр Queen в качестве контекста данных. Класс Queen должен сообщать TextBox об обновлении своего свойства StatusReport. Для этого мы обновим классQueen так, чтобы он реализовал интерфейс INotifyPropertyChanged. У рассмотренной нижефункциональности WPF не существует эквивалента для Mac, поэтому в приложении «Visual Studio для пользователей Mac» этот раздел пропущен.
440 глава 7 связывание данных с элементами Связывание данных в системе управления ульем Чтобы добавить связывание данныхв приложениеWPF, необходимовнести ряд изменений. Измените класс Queen, чтобы он реализовал интерфейс INotifyPropertyChanged. Обновитеобъявление класса Queen, чтобы онреализовал интерфейс INotifyPropertyChanged. Этотинтерфейс принадлежит пространству имен System.ComponentModel, поэтому в начало класса следует добавить директиву using: using System.ComponentModel; Теперь можно добавитьINotifyPropertyChanged в конец объявления класса. IDE подчеркивает объявление красной волнистой линией,чего и следовало ожидать, таккак вы ещене реализо- вали интерфейс ине добавили его компоненты. НажмитеAlt+Enter или Ctrl+,чтобы вывести потенциальные исправления, и выберите вкон- текстном менюкоманду «Implement Interface». IDEдобавляет вкласс строкукода с ключевым словом event,которое вам ещеневстречалось: public event PropertyChangedEventHandler PropertyChanged; Изнаетечто?Выужепользовались событиями!У классаDispatchTimer,использованноговгла- ве 1,было событиеTick,а элементыкнопокWPFиспользовалисобытиеClick.ТеперьклассQueen содержит событиеPropertyChanged.Любойкласс, которыйиспользуетсядлясвязыванияданных, инициирует (выдает) событие PropertyChanged, чтобы сообщить WPF об изменении свойства. Класс Queen должен инициировать свое событие по аналогии с тем, как DispatchTimer ини- циирует своесобытиеTick с заданным интервалом, а кнопка Button инициируетсвое событие Click,когда пользователь щелкаетна ней.Добавьте метод OnPropertyChanged: protected void OnPropertyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } Теперь необходимо изменить метод UpdateStatusReport для вызова OnPropertyChanged: private void UpdateStatusReport() { StatusReport = $"Vault report:\n{HoneyVault.StatusReport}\n" + $"\nEgg count: {eggs:0.0}\nUnassigned workers: {unassignedWorkers:0.0}\n" + $"{WorkerStatus("Nectar Collector")}\n{WorkerStatus("Honey Manufacturer")}" + $"\n{WorkerStatus("Egg Care")}\nTOTAL WORKERS: {workers.Length}"; OnPropertyChanged("StatusReport"); } 1 Мы добавили событие в класс Queen, а также метод, использующий оператор ?.для инициирования событий. Вот и все, что необходимо знать о событиях, — в ко нце книги приводится ссылка на главу, в которой событиярассматриваются более подробно. Сде лай те эт о!
дальше 4 441 приведение типов интерфейсов и is Измените код XAML, чтобы создать экземпляр Queen. Ранее вы создавали объекты ключевым словом new, а также пользовались методом Unity Instantiate.XAMLпредоставляет еще один способсозданияновыхэкземпляров классов.Добавьте следующий фрагмент в XAML непосредственно перед тегом <Grid>: <Window.Resources> <local:Queen x:Key="queen"/> </Window.Resources> Затемизменитетег <Grid> и добавьте к нему атрибут DataContext: <Grid DataContext="{StaticResource queen}"> Наконец, добавьтеатрибутTextк тегу <TextBox>, чтобысвязать его сосвойствомStatusReport класса Queen: <TextBox Text="{Binding StatusReport, Mode=OneWay}" Теперь элемент TextBox будет автоматически обновляться каждый раз, когда объект Queen будет инициировать свое событиеPropertyChanged. Измените код программной части, чтобы использовать экземпляр Queen в ресурсах окна. Покаполеqueen вфайлеMainWindow.xaml.cs содержитинициализатор поля с командой new длясозданияэкземпляраQueen. Мыизменимего, чтобывместо этогоиспользовалсяэкземпляр, созданный вXAML. Дляначалазакомментируйте(илиудалите)тривхождениястроки, присваивающейstatusReport. Text. Одна строка находится в конструктореMainWindow, а две другие — в обработчиках со- бытий Click: // statusReport.Text = queen.StatusReport; Затемизменитеобъявление поляQueen иудалите инициализатор поля(new Queen();)вконце: private readonly Queen queen; Наконец,измените конструктор, чтобыонзадавалзначение поляqueen следующим образом: public MainWindow() { InitializeComponent(); queen = Resources["queen"] as Queen; //statusReport.Text = queen.StatusReport; timer.Tick += Timer_Tick; timer.Interval = TimeSpan.FromSeconds(1.5); timer.Start(); } Вкодеиспользуетсясловарь сименемResources.(Мынемногозабегаемвперед —выузнаетео словарях вследующей главе.)Запустите игру.Она ведет себяточнотакже,каки прежде, нотеперьTextBox обновляется автоматически каждыйраз, когда Queen обновляет отчет о текущем состоянии. Поздравляем! Вы только что использовали интерфейс для добавления связывания данных в при ложение WP F. 2 3 Этоттег создает новый экземплярQueen идобав- ляет его в ресурсы вашего окна (механизм, при- меняемый окнами WPFдля хранения ссылок на объекты, используемые элементами управления). Теперь в приложении WPF используется связывание данных, и нам не нужно применять свойство Text для обновления элемента TextBox с отчетом. Заком- ментируйте или удалите эту с тр оку.
442 глава 7 один объект может иметь несколько типов В: Кажется, я понимаю все , что было сделано. А вы можете кратко пройтись по изменениям на случай, если я что -то упустил? О: Безусловно. Приложение системы управления ульем, построенноев главе6,обновляетэлементTextBox (statusReport), задавая его свойство Text в коде: statusReport.Text = queen.StatusReport; Мыизменилиприложение,чтобы в нем использовалось связывание данных для автоматического обновления TextBox каждый раз, когда объект Queen обновляет свое свойство StatusReport. Для этого были внесены три изменения.Преждевсего, класс Queenреализовал интерфейс INotifyPropertyChanged, чтобы уведомлять пользовательский интерфейс о любых изменениях свойства. Затем в коде XAML был создан экземпляр Queen, а свойство TextBox.Text связано со свойством StatusReport объекта Queen. Наконец, мы изменили код программной части, чтобыиспользовать экземпляр, созданный вXAML, и удалилистроки, в которых зада- валось значение statusReport.Text. В:Какую именно задачурешает этот интерфейс? О: И нте рфейс INo ti fyPr ope rty Ch anged п редос т авляе т возможность сообщить WPF о том, что свойство изме- нилось, чтобыприложение моглообновить связанныес этимсвойствомэлементы.Реализуяэтот интерфейс,вы строитекласс,который можетрешатьконкретную задачу: о повещен ие пр иложе ний WPF об изме нениях сво йс тв. Интерфейс состоит из одного компонента — события с именем PropertyChanged. Когда класс используется для связывания данных, WPF проверяет, реализует ли он интерфейс INotifyPropertyChanged, и если реа- лизует — присоединяет обработчик события к событию PropertyChanged вашего класса(по аналогии с тем, как вы связывали обработчики событий с обработчиками Click элементовButton). В: Язаметил, что при открытии окнав визуальном конструкторе элементTextBox с отчетом о текущем состоянии уже не пуст. Почему — из -за связывания данных? О:Вот это наблюдательность!Да, когда вы изме- нили XAMLидобавилисекцию <Window.Resources> для создания нового экземпляра объекта Queen, кон- структорXAMLв VisualStudio создал экземпляр объекта. Когдавы изменилиGrid,добавиликонтекстданных и соз- дали связывание сосвойством Text элемента TextBox, визуальный конструктор использовал эту информацию для вывода текста. Следовательно, когда вы исполь- зуете связывание данных, экземпляры ваших классов создаютсянетолькопри выполнении программы. Visual Studioсоздает экземпляры объектов приредактирова- нии окна XAML.Эточрезвычайномощнаявозможность IDE, потому что она позволяет вам изменить свойства в коде и увидеть результаты в конструкторе сразу же при построении кода. Связывание дан- ных работает со свойствами, но не с поля ми. Связывание данных может исполь- зоваться только с открытыми свойствами. Если вы попытаетесь связать атрибут элемента WPF с открытым полем, в программе ниче- го не изменится, — впрочем, исключе - ния вы тоже не получите. часто Задава ем ые вопросы Будьте осторожны!
дальше 4 443 приведение типов интерфейсов и is «Полиморфизм» означает, что один объект может существовать в разных формах Каждый раз, когда вы используетеRoboBee вместо IWorker, или Wolf вместо Animal, или даже выдержанный вермонтский чеддер врецепте, в котором требуется любой сыр, вы используете полиморфизм. И это жепроисходит прикаждом повышающем илипонижающем приведении типа. Выберетеобъект и используете его вметоде или вкоманде, кото- рыеожидают получить что-то другое, — это называется полиморфизмом. Полиморфизм вокруг нас Выуже давно пользуетесьполиморфизмом— простомыне использовали этот термин. Когда выбудетеписать коддляоставшихся глав,обращайте внимание на разнообразные возможности его использования. Нижеперечислены четыре типичных сценария использования поли- морфизма. Мыприведемпример длякаждого случая,хотявынеувидите эти конкретные строки вупражнениях. Когда вам попадется похожий код в упражнениях последних глав книги, вернитесь к этой странице исверьтесь со списком: Ссылочнойпеременной,использующей одинкласс,при- сваивается экземпляр другого класса. NectarStinger bertha = new NectarStinger(); INectarCollector gatherer = bertha; Вы используете поли- морфизм каждый раз, когда берете экзем- пляр одного класса и используете его в команде или в ме- тоде, рассчитанных на другой тип, на- пример родительский кла сс или ин терфейс, реализуемый классом. Повышающее приведение сиспользованиемсубклассав команде илив методе, рассчитаннымина егобазовыйкласс. spot = new Dog(); zooKeeper.FeedAnAnimal(spot); Создание ссылочной переменной с типом интерфейса иприсваиваниеей объекта, реализующегоэтотинтерфейс. IStingPatrol defender = new StingPatrol(); Понижающееприведениетипа с ключевым словом is. void MaintainTheHive(IWorker worker) { if (worker is HiveMaintainer) { HiveMaintainer maintainer = worker as HiveMaintainer; ... Если FeedAnimal ожидает получить объект Animal, а Dog наследует от Animal, вы можете передать Dog FeedAnimal. Метод MaintainTheHive получает любую реализацию IWorker в па- раметре. Он использует ключе- вое слово «as» для того, чтобы ссылка на HiveMaintainer указыва- ла на worker. Это тоже повышающее при- ведение типа!
444 глава 7 объектно-ориентированное программирование Кажется, я начинаю понимать, как работать с объектами! Вы занимаетесь объектно-ориентированным программированием. Программирование,которым вы занимаетесь, называется объектно-ориентированным(ООП.)До широкогораспро- странения таких языков, как C#,программисты не исполь- зовалиобъекты иметоды при написании своего кода. Они просто вызывалифункции(аналоги методовв необъектно- ориентированных программах), которые хранились в од- ном месте,словно каждая программабыла одним большим статическим классом, состоявшим только из статических методов. Такой подход серьезно усложнял написание про- грамм, моделировавшихрешаемые задачи. К счастью, вам уженепридетсяписать программыбезиспользованияООП, являющегося ключевойчастьюC#. Четыре основных принципа объектно-ориентированного программирования Когдапрограммистыговорят обООП, ониобычно имеют в видучетыре важныхпринципа. Каждыйизэтихпринципов долженбыть вам хорошо знаком, потому что выиспользоваликаждыйизнихвсвоихпрограммах. Мыужеупоминалио полиморфизме,а первыетри принципа знакомы вам по главам 5и6: наследование, абстракция и инкапсуляция. Полиморфизм Инкапсуляция Наследование Абстракция Означает создание объекта, который хранит внутреннюю информацию о своем состо- янии в приватных полях. При этом открытые свойства и методы используются для того, чтобы другие классы могли работать только с той частью внутренних дан- ных, которую им разрешено видеть. Абстракция используется при создании модели классов, которая начинается с самых общих (или абстрактных) классов, а затем переходит к более конкретным классам, наследующим от них. Термин «полимор- физм» буквально означает «много форм». Сможете ли вы представить си- туацию, в которой объект принимает несколько разных форм в вашем коде? Означает, что класс или ин- терфейс наследует от друго- го класса или интерфейса. Идея объединения данных казалась революционной в тот момент, когда она была впервые предложена. Впрочем, мы строили все программы C# именно так, и вы можете относиться к ней как к обычному про- граммированию.
Перечисления и коллекции 8 Организация данных ... И в этой сцене массовка выстраивается в очередь по росту. Всем занять свои места! Эй! Почемуникто нестроится? Шевелитесь,время — деньги. Э-э-э... Кто-нибудь меня слышит? Данные не всегда бывают такими аккуратными и ухоженными, как нам хотелось бы. В реальном мире данные, как правило, не хранятся маленькими ак- куратными кусочками. Нет, данные поступают вагонами, штабелями и кучами. Для их систематизации нужны мощные инструменты, и тут вам на помощь приходят пере- числения и коллекции. Перечисления — типы, позволяющие определять значения для классификации ваших данных. Коллекции — специальные объекты, способные хранить и сортировать данные, которые обрабатывает программа, и управлять ими. В результате вы можете сосредоточиться на основной идее программирования, оста - вив задачу управления данных коллекциям.
446 глава 8 Этот код компилируется, но такие «масти» и «номиналы» совершенно бессмысленны. Класс Card не должен допускать такие типы, как действи- тельные данные. строки могут содержать некорректные данные C ard Suit V alue N ame пе-ре-чис-лять, глагол. указывать элементы в опреде- ленном порядке. Строки не всегда подходят для хранения категорий данных В нескольких следующих главах мы будем работать с игральными картами; давайте создадим для них класс Card.Дляначала создайтеновыйклассCardс конструктором,которыйполучаетмасть иноминал карты исохраняетих в строковом виде: class Card { public string Value { get; set; } public string Suit { get; set; } public string Name { get { return $"{Value} of {Suit}"; } } public Card(string value, string suit) { Value = value; Suit = suit; } } Пока выглядит неплохо. Вы можете создать объектCard и использовать его следующим образом: Card aceOfSpades = new Card("Ace", "Spades"); Console.WriteLine(aceOfSpades); // Выводит Ace of Spades Но тут возникает проблема. Использование строк для хранения мастей и номиналов может иметь не- предвиденныезначения: Card duchessOfRoses = new Card("Duchess", "Roses"); Card fourteenOfBats = new Card("Fourteen", "Bats"); Card dukeOfOxen = new Card("Duke", "Oxen"); Впринципе, можно былобы добавить в конструктор код, который проверяет каждую строкуиубеждается в том, что она представляетдействительную масть или номинал и выдает исключение при некоррект- ных входных данных. Такоерешение вполне допустимо — конечно, если вы организуете корректную обработку исключений. Но разве небылобыудобнее, если бы компиляторC# мог автоматически обнаруживать недействительные значения?Еслибыкомпилятор моггарантировать,чтовсе мастии но- миналыдействительны,еще довыполнениякода?Представьте, такая возможность существует!Все, что для этого нужно, — использовать перечислениедлядопустимых значений. Класс Card использует стро- ковые свойства для хранения мастей и номиналов. Карта «Герцог быков». В природе не встречается.
дальше 4 447 перечисления и коллекции Перечисления предназначены для работы с наборами допустимых значений Перечисление (или перечисляемый тип) — тип данных, допускающий ограниченный набор допусти- мых значений для некоторых данных. Таким образом, мы можем определить для карточных мастей перечислениес именемSuits и следующие допустимыемасти: enum Suits { Diamonds, Clubs, Heart s, Spade s, } Перечисление определяет новый тип Используяключевое словоenum, выопределяетеновыйтип. Несколькополезных фактов,которыенеобходимо знать о перечислениях: Перечисление может использоваться как тип в определении переменной — такой же, как строка, int или любой другой тип: Suits mySuit = Suits.Diamonds; Так как перечисление является типом, оно может использоваться для со зд ани я масси ва: Suits[] myVals= new Suits[3] { Suits.Spades, Suits.Clubs, mySuit }; Для сравнения значений из перечислений может использоваться оператор ==. Следующий метод получает перечислениеSuit в параметре и использует == для проверки его на равенство сSuits.Hearts: void IsItAHeart(Suits suit) { if (suit == Suits.Hearts) { Console.WriteLine("You pulled a heart!"); }else{ Console.WriteLine($"You didn't pull a heart: {suit}"); } }t В перечисление нельзядобавить новое значение. Если вы попытаетесь это сделать, программа небудет компилироваться — это позволит избежать некоторых неприятных ошибок: IsItAHeart(Suits.Oxen); При попытке использовать значение, не входящее в перечисление, компилятор выдастошибку: ✓ ✓ ✓ ✓ Вообще говоря, запя- тая за последним эле- ментом перечисления необязательна, но она упрощает возможные перемещения элемен- тов методом копиро- вания/вставки. Перечисление начинается с ключевого слова enum, за которым следует имя. Это перечисление называется Suits. Далее в перечислении указываются элементы, разделенные запятыми и заключенные в фигурные скобки. Для каждого уникального значе- ния — в данном случае для каждой масти — определяется один элемент. Перечисление о пр еде ляет новый тип, который мо- жет содержать значения из фиксиро в а н- ного набора. Любое значе- ние, которое не входит в перечисле- ние, нарушит работоспособ- ность кода, что может предот- вратить воз- можные ошиб- ки в будущем. Метод ToString перечисления возвращает эквивалентную строку, т. е. Suits.Spades.ToString возвращает "Spades".
448 глава 8 Перечисления позволяют представлять числа именами Иногда бываетудобнееработать с числами, еслиприсвоить им имена. Вы можетесвязать числа со зна- чениями в перечислении и использовать имена для обращения к ним. В этом случае вам не придется иметь дело сзагадочными числами, неожиданнопоявляющимисяв вашем коде. Следующее перечисление позволяет отслеживать оценки за выполнениеразличных команд вконкурсе для собак: enum TrickScore { Sit=7, Beg = 25, RollOver = 50, Fetch = 10, ComeHere = 5, Speak = 30, } Фрагментметода, которыйиспользует перечисление TrickScore преобразова- нием его кintиобратно: int score = (int)TrickScore.Fetch * 3; // Следующая строка выводит: The score is 30 Console.WriteLine($"The score is {score}"); Перечисление можно преобразовать в число и выполнить с ним вычисления. Его даже можно преобразоватьвстроку — методToString перечисления воз- вращаетстрокус именем элемента: TrickScore whichTrick = (TrickScore)7; // The next line prints: Sit Console.WriteLine(whichTrick.ToString()); Есликакое-тоимянесвязано счислом,элементам списка присваиваютсязначе- нияпо умолчанию.Первомуэлементубудетприсвоенозначение0,второму—1, ит. д. Но что произойдет,еслиодномуиз перечисленийпотребуетсяприсвоить очень большое число?По умолчанию для типов перечислений используется типin, поэтомунужныйтип необходимо задать оператором(:), каквследующем примере: enum LongTrickScore : long { Sit=7, Beg = 2500000000025 } Элементы не обязатель- но указывать в каком-то определенном порядке, и с од- ним числом может быть связано несколь- ко имен. Укажите имя, за ним «=», а потом ч ис ло, котор ое представляется этим именем. Сообщает компилятору, что значения в TrickScore должны интерпрети- роваться с типом long, а не int. имена могут быть полезнее номеров Если вы попытаетесьиспользовать это перечисление без указания типа long, произойдетошибка: Приведение типа(int) приказывает компиля- тору преобразовать имя в число, которое оно представляет. Таким образом, поскольку Tri ckScor e.F et ch им е- ет значение 10, (int) Tri ckSco re. Fet ch пр еоб - разует его в значение 10 типа int. int можно преобразовать обратно в TrickScore, а TrickScore.Sit представ- ляет значение 7. Console.WriteLine вызывает метод ToString перечис- ления; этот метод возвращает строку с именем элемента. Число слиш- ком велико для типа int int можно преобразовать в перечисление, и перечис- ление (набазе int) можно преобразовать в int. Некоторые перечисления ис- пользуют другиетипы вме- сто int, например byte или long (см. ниже). Их можно преобразовать к их базово- му типу вместо int.
дальше 4 449 перечисления и коллекции Используйте то, что вы узнали о перечислениях, для построения класса, представляюще- го игральную карту. Создайте новый проект консольного приложения .NET Core Console App и добавьте в него класс с именем Card. Добавьте в Card два открытых свойства: Suit (допустимые значения Spades, Clubs, Diamonds и Hearts) и Value (Ace, Two, Three... Ten, Jack, Queen, King). Также понадобится еще одно свой- ство: открытое свойство, доступное только для чтения, с именем Name, которое возвращает строку вида «Ace of Spades» или «Five of Diamonds». Добавьте два перечисления для определения мастей и номиналов в отдельных файлах *.cs Добавьте перечисления. В Windows используйте знакомую функцию Add>>Class, а затем за - мените class на enum в каждом файле. В macOS используйте команду Add>>New File... и выберите Empty Enumeration. Скопируйте перечисление Suits, которое мы привели, затем создайте перечисление для номина- лов. Проследите за тем, чтобы номиналы соответствовали картам: значение (int)Values.Ace всегда должно быть равно 1, Two — 2, Three — 3 и т. д . Номинал Jack должен соответствовать 11, Queen — 12 и King — 13 . Добавьте конструктор и свойствоName, возвращающее строку с именем карты Добавьте конструктор, который получает два параметра, Suit и Value: Card myCard = new Card(Values.Ace, Suits.Spades); Свойство Name должно быть доступно только для чтения. Get-метод должен возвращать строку с описанием карты. Таким образом, код: Console.WriteLine(myCard.Name); должен выводить следующий результат: Ace of Spades Выведите имя случайной карты в методе Main Чтобы ваша программа сгенерировала карту со случайной мастью и номиналом, преобразуйте случайное число от 0 до 3 для перечисления Suits и еще одно случайное число от 1 до 13 для перечисления Values. Для этого можно воспользоваться встроенным классом Random, который предоставляет три разных способа вызова следу- ющего метода Next: Random random = new Random(); int numberBetween0and3 = random.Next(4); int numberBetween1and13 = random.Next(1, 14); int anyRandomInteger = random.Next(); В:Япомню, что ранее в книге метод Random.Next вызывался с двумя аргументами. Я заметил, что при вызове метода по- является окно IntelliSense, в углу которого написано «3 из 3». Этокак-то связано с перегрузкой? О: Да! Когда класс содержит переопределенный метод — или метод, который может вызываться несколькими способами, — I DE сообщает вам обовсех имеющихся возможностях.В данном случае класс Random содержит три возможные реализации метода Next. Как только вы вводите в окне кода random.Next(, IDE открывает о кно Int ell i S ens e с парамет рами разны х переопред еленны х мет одов . Стрелкии ‚рядомс надписью «3 из3»позволяютпросматривать варианты. Это особенно полезно при работе с методами, имею - щимидесятки перегруженных определений.Таким образом, когда вы вызываете Random.Next, убедитесь в том, что вы выбираете правильный перегруженный метод.Но пока не стоит отвлекаться на это — перегрузка гораздо подробнеерассматривается позднее в этой главе. Car d Value Suit Name Вы делали это в главе 3. Этот вызов приказывает Random вернуть значение в диапазоне от1до14. Если один метод может быть вызван несколькими спо- собами, это назы - вается перегрузкой (overloading). Чтобы добавить перечисление в Visual Studio для Mac, добавь- те файл и вы- берите тип файла «Empty Enumeration». Упражнение часто Задаваемые воп ро сы
450 глава 8 Колода карт — отличный пример программы, в которой очень важно ограничивать возможные значе- ния. Никто не захочет взять свои карты и увидеть среди них 28 червей или джокер треф. Ниже при- ведена наша реализация класса Card — она будет несколько раз использована в следующих главах. ПеречислениеSuits определяется вфайле с именем Suits.cs. Код перечисления у вас уже есть — он идентичен перечислению Suits, которое было приведено ранее в этой главе. Перечисление Values хранится в файле с именем Values.cs: enum Values { Ace=1, Two=2, Three = 3, Four = 4, Five = 5, Six=6, Seven = 7, Eight = 8, Nine = 9, Ten = 10, Jack = 11, Queen = 12, King = 13, } Класс Card имеет конструктор, задающий свойства Suit и Value, и свойство Name, генерирующее строку с опи- санием карты: class Card { public Values Value { get; private set; } public Suits Suit { get; private set; } public Card(Values value, Suits suit) { this.Suit = suit; this.Value = value; } public string Name { get { return $"{Value} of {Suit}"; } } } В классе Program используется статическая ссылка Random с приведением типов Suits и Valuesдля создания случайной карты Card: class Program { private static readonly Random random = new Random(); static void Main(string[] args) { Card card = new Card((Values)random.Next(1, 14), (Suits)random.Next(4)); Console.WriteLine(card.Name); } } Здесь Value. Ace при- сваивается значение 1. Get-метод свойства Name использует тот факт, что метод ToString перечисления возвращает свое имя, преоб- разованное в строку. массивы... стоит ли игра свеч? А Values.King присваивается значение 13. Пример инкапсуляции. Мы объявили set-методы для свойств Value и Suit при- ватными, потому что они должны вызываться только из конструктора. Таким об- разом предотвращается их случайное изменение. Перегруженный метод Random.Next используется для генерирования случайного числа в диапазоне от 1 до 13. Результат преобразуется в значение Values. Мы выбрали для перечислений имена Suits и Values, тогда как свойства класса Card, ис- пользуемые этими перечислениями для типов, называются Suit и Value. Что вы скажете об этих именах? Найдитеименадругихперечислений, кото- рыевстречаются в книге. Не лучше ли было бы присвоить им именаSuit и Value? Правильного или неправильного ответа не суще- ствует — собственно, в справочнике Microsoft по языку C# присутствуют формы как одиночного (например, Season), так и множественного числа (например, Days): https://docs.microsoft.com/en-us/dotnet/csharp/language- reference/builtin-types/enum. Упражнение Решение
дальше 4 451 перечисления и коллекции Для создания колоды карт можно воспользоваться массивом... Аесливыхотитесоздать класс,представляющийколодукарт?Дляэтогонеобходимо отслеживать каждую карту вколоде, а также знать, вкакомпорядке следуюткарты. Впринципе, дляэтого можно былобывоспользоватьсямассивомCards —верхняякарта колоды хранитсявэлементе с индексом0, следующая — в элементе с индексом 1, ит. д. Следующий класс мог бы стать отправной точкой — объект Deck,которыйначинаетсяс полнойколоды из 52карт: class Deck { private readonly Card[] cards = new Card[52]; public Deck() { int index = 0; for (int suit = 0; suit <= 3; suit++) { for (int value = 1; value <= 13; value++) { cards[index++] = new Card((Values)value, (Suits)suit); } } } public void PrintCards() { for (int i = 0; i < cards.Length; i++) Console.WriteLine(cards[i].Name); } } .. . но что, если массива окажется недостаточно? Представьте все, что можно сделать с колодой карт. Если вы играете в карточную игру, вам по- стоянно придется изменять порядок карт, добавлять иудалять карты из колоды. С обычным массивомделать этоне так просто. Например,возьмемметодAddWorkerиз системы управления ульем в главе 6: private void AddWorker(Bee worker) { if (unassignedWorkers >= 1) { unassignedWorkers--; Array.Resize(ref workers, workers.Length + 1); workers[workers.Length - 1] = worker; } } Приходилось вызывать Array.Resize для расширения массива,а потом добавлять новогорабочего вконец. Слишком много лишнейработы. А как вы реализуете методShuffle, который переставляет карты в случайном порядк е? Как насчет методадля сдачи первой карты, который возвращает к арту иудаляет ее из колоды? Как реализовать добавление карты в колоду? Мы использовали два цикла «for» для перебора всех воз- можных комбина- ций масти и номи- на ла. Этот код использовался для добавления элемента в мас- сив в главе 6. А что будет, если вам потребуется доба- вить ссылку на Bee в середину, а не в конец массива? Мозговой штурм
452 глава 8 С массивами бывает неудобно работать Массивхорошо подходитдля фиксированного набора значений или ссылок. Но есливам потребуется перемещать элементы или добавлять новые элементы, не помещающиеся в массиве,начинаются про- блемы. Рассмотрим несколько ситуаций,вкоторыхработа с массивамиможетоказаться неудобной. У каждого массива имеется длина. Она не изменяется при изменении размера массива, а значит, вы должнызнать длинумассивадляработы с ним. Допустим,выхотите использовать массивдляхранения ссылок на Card.Если количествоссылок, которыенеобходимо хранить,меньше длины массива,можно воспользоваться null-ссылкамидляпредставления пустых элементов. Ситуацияусложняется.Выможете легкодобавить метод Peek, который возвращает ссылку наверхнюю карту, и выможете узнатькартусверхуколоды.А еслипотребуетсядобавить карту?Если cardCount меньше длины массива, можно просто поместить карту в массив с этим индексом и увеличить cardCountна единицу.Но если массив заполнен, придется создать новый массивбольшегоразмераи скопировать в него существующие карты. Уда- литькартунесложно— но после того,какcardCountуменьшитсяна единицу, необходимо позаботитьсяотом, чтобыпоиндексуудаленной картыхранилось значение null.Аесли потребуетсяудалить картувсерединесписка?Если выудаляете карту4,необходимосме- стить карту5 на открывшееся место, затемкарту6, карту7...Нет, тольконе это! О б ъ е к т C a r d О б ъ е к т C a r d О б ъ е к т C a r d Необходимо отслеживать количество карт, хранимых в массиве. Можно добавить полеint(допустим, с именем cardCount), в котором будет храниться индекс последней карты в массиве. Таким образом, массивс тремя картамибудетиметь длину(Length)7,но полеcardCountбудетсодержать3. Длина (Length) массива равна 7, но сейчас в нем хранятся всего три карты. Элементы с ин- дексами3,4,5и6 содержат «null», т. е . не содержат кар т. Можно добавить поле cardCount для хранения количества карт в массиве. Элемент с любым индексом, превыша- ющим cardCount, содержит null-ссылку. В методе AddWorker из главы 6 д ля этой цели ис- пользовал- ся метод Array.Resize. в списках можно хранить что угодно Что произойдет, если значение cardCount рас- синхронизируется с мас- сивом? Ошибки, конечно!
дальше 4 453 перечисления и коллекции В списках можно хранить коллекции... чего угодно ВC#и .NET существуетмножество классовколлекций,позволяющихлегко решить не- приятные вопросы с добавлением иудалением элементовмассива. Чащевсего исполь- зуетсяколлекцияList<T>. СоздавобъектList<T>,высможетелегкодобавлять иудалять элементы из произвольной позиции списка, получать значение отдельного элемента идаже перемещатьэлемент из одной позиции в другую. Посмотрим,какработают списки. Начните с создания нового экземпляра List<T>. Вспомните, что у каждого массива есть тип: выработаетене просто с массивом,а с массивом int,массивом Cardит.д .Со списками дело обстоитаналогично. Выдолжны указать тип объек- тов или значений, которые должны храниться в списке, в угловых скобках(<>) присоздании списка ключевым словом new: List<Card> cards = new List<Card>(); Теперь можно перейти к добавлению элементов в List<T>. После того как у вас появится объект List<T>, вы можете добавить в него сколько угодно элементов — приусловии,что ониполиморфныпотипу,заданномупри созданииList<T>, иначе говоря,что онисовместимы стипом по присваиванию(к этой категорииотносятся интерфейсы,абстрактныеибазовыеклассы). cards.Add(new Card(Values.King, Suits.Diamonds)); cards.Add(new Card(Values.Three, Suits.Clubs)); cards.Add(new Card(Values.Ace, Suits.Hearts)); 1 2 О б ъ е к т C a r d О б ъ е к т L i s t < C a r d > При создании списка был указан тип <Card>, поэтому в списке могут храниться только ссыл- ки на объекты Card. О б ъ е к т C a r d О б ъ е к т L i s t < C a r d > О б ъ е к т C a r d King of Diamonds 3of Clubs Aceof Hearts В List можно добавить сколько угодно объектов Card — для этого доста- точно вызвать метод Add. Объект List сам позабо- тится о том, чтобы для элементов было достаточ- но «ячеек». Если свободное место в List будет исчер- пано, объект автомати- чески расширится. Список поддерживает определенный порядок своих элементов, как и мас- сив. Король бубен (King of Diamonds) находится на первом месте, трой- ка треф (3 of Clubs) — на втором, а туз червей (Ace of Hearts) — на третьем. Иногда мы будем опу- скать <T> в упоминаниях List. Когда вы видите в тек- сте List, счи - тайте, что это List<T>. <T> в конце List<T> означает, чтотипявляется обобщением. T заменяется конкретным типом — таким образом, List<int> попро- сту означает List с элементами int. На нескольких ближайших страницах у вас будет возможность потренироватьсявис- пользованииобобщений. Значения или ссылки на объ- екты, содержащиеся в списке, обычно называются элементами. РЕЛАКС
454 глава 8 Списки обладают большей гибкостью, чем массивы КлассListвстроен в.NETFramework.Он позволяет делать собъекта- ми много такого, что былоневозможно страдиционными массивами. Перечислим некоторые возможностиList<T>: Использование ключевого слова new для создания экземпляра List (как и следовало ожидать): List<Egg> myCarton = new List<Egg>(); 1 Добавление элементов в список. Egg x = new Egg(); myCarton.Add(x); 2 Добавление других элементов в список. Egg y = new Egg(); myCarton.Add(y); 3 Определение количества элементов в списке. int theSize = myCarton.Count; 4 Определение позиции этого объекта в List. int index = myCarton.IndexOf(x); 6 Удаление объекта из списка. myCarton.Remove(x); 6 Проверка наличия конкретного объекта в List. bool isIn = myCarton.Contains(x); 5 Список расширяется для хранения объекта Egg... ...и снова расширяется для хранения второго объ ект а Egg. Теперь вы можете провести поиск кон- кретного объекта Egg в List. Разумеется, по- иск вернет true, пото- му что вы только что добавили этот объект Egg в List. При удалении x в списке остается только y, размер списка уменьшился! Если уда- лить y, список вскоре будет уничтожен сборщиком мусора. Индекс x будет равен 0, а индекс y будет равен 1. хлоп! списки как улучшенная версия массивов new List<egg>(); созда- ет список List с объек- тами Egg. В исходном со стоян ии сп исо к пуст. Вы можете добавлять и удалять из него объ- екты, но так как список предназначен для объ- ектов Egg, добавлять можно только ссылки на объекты Egg или любые объекты, которые могут быть преобразованы в Egg. Ссылка на объект Egg. Другой объект Egg. x y x x y y
дальше 4 455 перечисления и коллекции List<String> myList = new List <String>(); String [] myList = new String[2]; String a = "Yay!"; String a = “Yay!”; myList.Add(a); String b = "Bummer"; String b = “Bummer”; myList.Add(b); int theSize = myList.Count; Guy o = guys[1]; bool foundIt = myList.Contains(b); Lis t Обычный массив Заполните пустые ячейки таблицы. Присмотритесь к коду List в левом столбце и запишите, как бы, по вашему мнению, вы- глядел этот код при использовании обычного массива. Мы не ожидаем,чтовыидеальносправитесьсо всемизаданиями, так что постарайтесь предложитьнаиболеевероятные варианты. Подсказка: здесь од- ной строки кода бу- дет недостаточно. Мы заполнили за вас пару ответов. Несколько строк из середины про- граммы. Предполагается, что эти команды выполняются последова- тельно, одна после другой, а пере- менные были объявлены ранее. Возьми в руку карандаш
456 глава 8 Lis t Обычный массив СпискиList — объекты, содержащие методы,ко- торые ничем не отличаются от других классов, использовавшихся до сих пор. Чтобы просмо- треть список доступных методов в IDE, просто введите . рядом с именем List. Параметры пере- даютсяэтим методамточно также, каки классам, написанным вами. Элементывспискеупорядочены;позицияэлемен- та всписке называется индексом. Как ив случае с массивом, индексы списков начинаются с 0. Дляобращения кэлементусписка с конкретным индексом используетсяиндексатор: Guy o = guys[1]; С массивами ваши возможности намного более ограниченны. Размер массива должен быть заданприего создании,ивсю логикувыполня- емых операций вам придется написать само- сто ятел ь но. Вам было предложено заполнить пустые ячейки:просмотрите код с List в левом столбце и попробуйте написать эквивалентный код собычным массивом. списки и массивы List<String> myList = new List <String>(); String[] myList = new String[2]; String a = "Yay!" String a = “Yay!"; myList.Add(a); myList[0] = a; String b = "Bummer"; String b = “Bummer"; myList.Add(b); myList[1] = b; int theSize = myList.Count; int theSize = myList.Length; Guy o = guys[1]; Guy o = guys[1]; bool foundIt = myList.Contains(b); bool foundIt = false; for (int i = 0; i < myList.Length; i++) { if (b == myList[i]) { isIn = true; } } Класс Array содержит статические методы, которые немного упрощают выполнение некоторых операций — на- пример, вы уже видели метод Array. Resize, использованный в методе AddWorker. Но мы сосредоточимся на объектах List, потому что с ними гораздо проще работать. Возьми в руку карандаш Решение
дальше 4 457 перечисления и коллекции The shoe closet is empty. Press 'a' to add or 'r' to remove a shoe: a Add a shoe Press 0 to add a Sne aker Press 1 to add a Loafer Press 2 to add a Sandal Press 3 to add a Fli pflop Press 4 to add a Win gtip Press 5 to add a Clog Enter a style: 1 En ter the co lor: black T he shoe cl oset conta ins: Shoe #1: A bla ck Loafer Press 'a' to add or 'r' to remove a shoe: a Add a shoe Press 0 to add a Sne aker Press 1 to add a Loafer Press 2 to add a Sandal Press 3 to add a Fli pflop Press 4 to add a Win gtip Press 5 to add a Clog Enter a style: 0 En ter the co lor: blue and white T he shoe cl oset conta ins: Shoe #1: A bla ck Loafer Shoe #2: A blu e and whit e Sneaker Press 'a' to add or 'r' to remove a shoe: r Enter the number of the shoe to remove: 2 R emoving A blue and w hite Sneak er T he shoe cl oset conta ins: Shoe #1: A bla ck Loafer Press 'a' to add or 'r' to remove a shoe: r Enter the number of the shoe to remove: 1 R emoving A black Loaf er The shoe closet is empty. Press 'a' to add or 'r' to remove a shoe: Построим приложение для хранения обуви Пришловремя применить список List на практике. По- строим консольное приложение .NET Core, которое предлагает пользователюдобавитьилиудалить изсписка пару обуви. Пример запуска приложения с добавлением двух пар обуви и их последующим удалением: Начнем скласса Shoe, в котором хранитсяфасон и цвет обуви. Затем мы создадим классс именем ShoeCloset, кото- рыйхранитданныеобувивспискеList<Shoe>, сметодами AddShoeиRemoveShoe для добавления иудаления обуви. Доб авьте пере ч исл ение для ф ас она обу ви. Обувь бываетразная: кроссовки, сандалиии т. д., так что перечисление имеетсмысл: enum Style { Sneaker, Loafer, Sandal, Flipflop, Wingtip, Clog, } Добавьте класс Shoe. В нем используется перечислениеStyle для фасона истрока дляцвета; вцелом класс работаеттак же,как класс Card, созданный ранее в этой главе: class Shoe { public Style Style { get; private set; } public string Color { get; private set; } public Shoe(Style style, string color) { Style = style; Color = color; } public string Description { get { return $"A {Color} {Style}"; } } } 1 2 Нажмите ‘a ’, чтобы до- бавить обувь, затем выбе- рите фасон и введите цвет. Нажмите ‘r’, чтобы удалить обувь, за- тем введите номер удаляемой пары. Вспомните, о чем говорилось ра- нее: перечисление можно привести к int и обратно. Таким образом, Sneaker равно 0, Loafer–1,ит.д. Сдела йт е это!
458 глава 8 использование списка в приложении Класс ShoeCloset использует List<Shoe> для управления данными обуви. Класс ShoeClosetсодержит триметода:методPrintShoes выводит списокобувина консоль, метод AddShoeзапрашиваетупользователяданныеобуви,включаемойв список,а методRemoveShoe предлагает пользователю удалить данные: using System.Collections.Generic; class ShoeCloset { private readonly List<Shoe> shoes = new List<Shoe>(); public void PrintShoes() { if (shoes.Count == 0) { Console.WriteLine("\nThe shoe closet is empty."); } else { Console.WriteLine("\nThe shoe closet contains:"); inti=1; foreach (Shoe shoe in shoes) { Console.WriteLine($"Shoe #{i++}: {shoe.Description}"); } } } public void AddShoe() { Console.WriteLine("\nAdd a shoe"); for(inti=0;i<6;i++) { Console.WriteLine($"Press {i} to add a {(Style)i}"); } Console.Write("Enter a style: "); if (int.TryParse(Console.ReadKey().KeyChar.ToString(), out int style)) { Console.Write("\nEnter the color: "); string color = Console.ReadLine(); Shoe shoe = new Shoe((Style)style, color); shoes.Add(shoe); } } public void RemoveShoe() { Console.Write("\nEnter the number of the shoe to remove: "); if (int.TryParse(Console.ReadKey().KeyChar.ToString(), out int shoeNumber) && (shoeNumber >= 1) && (shoeNumber <= shoes.Count)) { Console.WriteLine($"\nRemoving {shoes[shoeNumber - 1].Description}"); shoes.RemoveAt(shoeNumber - 1); } } } 3 Sho eCl oset pr iv ate Li st< Shoe> shoes P r intS hoes A ddS hoe R emoveS hoe Список List, в котором хра- нятся ссылки на объекты Shoe. Цикл foreach перебирает список «shoes», и для каждой пары обуви на консоль выво- дится строка. Цикл for присваивает «i» целое число в диапазоне от 0 до 5. Интерполиру- емая строка использует {(Style)} для приведения типа к перечислению Style, а затем вызывает метод ToString для вывода имени элемента. Такой код уже встречался вам ра- нее: он вызывает Console.ReadKey, а затем использует KeyChar для получения нажатой клавиши в виде символа. Метод int.TryParse полу- чает строку, а не символ, поэто- му char преобразуется в строку вызовом ToString. Здесь мы создаем новый экземпляр Shoe и до- бавляем его в список. Здесь экзем- пляр Shoe удаляется из списка. Незабудьтевключить строку using в начало кода, без нее вы не сможете использовать класс List.
дальше 4 459 перечисления и коллекции Добавьте класс Program с точкой входа. Пока вроде ничего особенного не происходит. Дело втом, что всеинтересноеповедениеинкапсулируетсявклассеShoeCloset: class Program { static ShoeCloset shoeCloset = new ShoeCloset(); static void Main(string[] args) { while (true) { shoeCloset.PrintShoes(); Console.Write("\nPress 'a' to add or 'r' to remove a shoe: "); char key = Console.ReadKey().KeyChar; switch (key) { case 'a': case 'A': shoeCloset.AddShoe(); break; case 'r': case 'R': shoeCloset.RemoveShoe(); break; default: return; } } } } Запустите приложение и добейтесь повторения приведенных результатов. Попробуйте отладить приложениеиразобратьсявтом, какработаютсписки. Ничего запоминать пока не нужно — у васбудетмасса возможностей потренироватьсяв их использовании! 4 5 Для обработки пользовательского ввода используется команда switch. Команда 'A' в верхнем регистредолжнаработать точно так же, как и команда 'a' в нижнем регистре, поэтому мы разместили два варианта case подряд, не разделяя их командой break: case 'a': case 'A': Когда switch переходит к новому варианту case, перед которым небыла выполнена командаbreak, происходит сквозной пере- ход к следующему варианту. Междудвумя командами case даже могут располагаться команды. Но будьтевнимательны — при этом очень легко пропустить нужную командуbreak. После варианта 'a' нет команды break, поэтому происхо- дит сквозная пере- дача управления к варианту 'A' — оба варианта обраба- тываются одним методом shoeCloset. AddShoe. Класс коллекции List содержит метод Add для добавления элемента в конец списка. Метод AddShoe создает экземпляр Shoe, а затем вызывает метод shoes.Add со ссылкой на этот эк- земпляр: shoes.Add(shoe); Класс List также содержит метод RemoveAt для удаления из списка элемента с заданным индексом. У списков, как и у массивов, индексы начинаются с нуля; иначе говоря, первому элементу соответствует индекс 0, второ- му—индекс1,ит.д. shoes.RemoveAt(shoeNumber - 1); Наконец, метод PrintShoes использует свойство List.Count для проверки того, пуст ли список: if (shoes.Count == 0) Элементы списка под увеличительным стеклом
460 глава 8 в обобщенной коллекции может храниться любой тип В обобщенных коллекциях могут храниться любые типы Выуже видели, что в списке могут храниться строки или объекты Shoe. Также можно создавать списки целых чисел или любых объектов, которые вы можете создать. В таком случае списокстановитсяобобщенной коллекцией.Присозда- ниинового объектаList выпривязываетеего кконкретномутипу: можно создать списокдляхранения элементовint, строкилиобъектовShoe. Такойподходупро- щает работу со списками: после того, как список будет создан, вы всегда знаете тип данных,которые внем хранятся. Ночто имеетсяв видупод термином «обобщенный»? Исследуем обобщенные коллек- циивVisualStudio.ОткройтефайлShoeCloset.csинаведите указатель мышинаList: Несколько фактов, на которые стоит обратить внимание: ‘ КлассListизпространства именSystem.Collections.Generic — этопростран- ство имен содержит классы обобщенных коллекций (именно поэтому не- обходима строка using). ‘ В описаниисказано, что List предоставляет «методы для поиска, сорти- ровкиивыполненияопераций со списками». Некоторыеиз этих методов использовались в нашемклассе ShoeCloset. ‘ ВверхнейстрокеупоминаетсяList<T>,а внижней —T is Shoe.Такопределя- ются обобщения —посути,в описаниисказано,чтоList можетработать слюбым типом, но дляэтогоконкретного списка такимтипомстановится классShoe. Обобщенные списки объявляются с <угловыми скобками> Когда вы объявляетесписок,какой бы тип в нем ни хранился,это всегда делается одинаково: тип объ- ектов,хранящихся всписке, задаетсяв <угловыхскобках>. Часто обобщенные классы (не только List) записываются в виде: List<T>. По этой записи сразу видно, что класс можетопределяться с любым типом элементов. List <T> name = n ew List<T>(); На самом деле это не означает, что вы добавляе- те букву T. Это всего лишь условная запись, которая встречается с классами и интерфейсами, работаю- щими с любыми типами. На место части <T> можно поместить любой тип (например, List<Shoe>); тем самым вы ограничиваете элементы указанным типом. Списки могут быть очень гибкими (с возможностью хранения любого типа), а могут быть предельно огра- ниченными. Они делают все, что могут делать масси- вы, и немало того, на что массивы не способны. В обобщенных ко лле кция х могут хранить- ся объекты лю- бого типа. Они предоставляют целостный набор методов для работы с объектами в кол лекци и независимо от того, объекты какого типа в ней хранятся. об-об-щен-ный, прил. Характеристика класса или группы объектов; некон- кретный.
перечисления и коллекции Класс List является частью .NET Core — огромной подборки очень полезных классов, интерфейсов, типов и т. д . В VisualStudio существует чрезвычайно мощный инструмент, при помощи которого мож- но исследовать эти классы и любой другой код, написанный вами. Откройте Program.cs и найдите следующую строку: static ShoeCloset shoeCloset = new ShoeCloset(); Щелкнитеправой кнопкой мыши на ShoeCloset и выберите команду Go To Definition (Windows) или Go To Declaration (macOS). В Windows так- же можно перейти к определению клас- са, компонента или переменной, щелкнув на нем с нажатой клавишей Ctrl. IDE немедленно переходит к определению класса ShoeCloset. Вернитесь к Program.cs и перейдите к определению PrintShoes в следующей строке: shoeCloset.PrintShoes();. IDE переходит прямо к определению метода в классе ShoeCloset. Команда Go To Definition/Go To Declaration позволяет быстро перемещаться по вашему коду. Используйте Go To Definition/Declaration для исследования обобщенных коллекций А теперь самое интересное. Откройте ShoeCloset.cs и перейдите к определению List. IDE открыва- ет отдельную вкладку с определением класса List. Не беспокойтесь, если новая вкладка содержит много сложного кода! Понимать все не обязательно — просто найдите следующую строку, из кото - рой видно, что List<T> реализует набор интерфейсов: public class List<[NullableAttribute(2)] T> : ICollection<T>, IEnumerable<T>, IEnumerable, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection, IList Заметили, что на первом месте стоит интерфейс ICollection<T>? Этот интерфейс используется все- ми обобщенными коллекциями. Вероятно, вы уже поняли, что делать дальше — перейти к опре- делению/объявлению ICollection<T>. Ниже показано, что вы увидите в Visual Studio для Windows (комментарии XML свернуты и заменены кнопками ; на Mac их можно раскрыть: В предыдущей главе мы говорили о том, что суть интерфейсов — выполнение классами конкретных задач. Функциональность обобщенной коллекции является конкретной задачей. Любой класс мо- жет обладать этой функциональностью, для чего достаточно реализовать интерфейс ICollection<T>. Этот интерфейс реализуется классом List<T>; в этой главе вы увидите еще несколько классов кол- лекций, которые тоже его реализуют. Все они работают по-разному, но поскольку все они обладают функциональностью обобщенной коллекции, вы можете быть уверены в том, что все они поддержи- вают базовые возможности сохранения значений или ссылок. Подсказки для IDE:Go To Definition (Windows) /GoTo Declaration (macOS) Обобщенная коллекция предоставляет воз- можность узнать, сколько элементов в ней хранится; добавить новые элементы; очи- стить; проверить, содержит ли она заданный элемент; а также удалить элемент. Также поддерживаются другие возможности (напри- мер, List позволяет удалить элемент с кон- кретным индексом), но этот минимальный стандартный набор подд ерживается каждой обобщенной коллекцией.
462 глава 8 ¢ List — класс .NETдля хранения, управления и удоб- ной работы с наборами значений или ссылок на объ- екты. Значения или ссылки, хранящиеся в списке, на- зываются элементами. ¢ Размер List изменяется динамически под нужный размер. При добавлении данныхListрасширяется по мере надобности. ¢ Чтобы поместить объект в List, используйте метод Add.Для удаления используется метод Remove. ¢ Дляудаленияобъектов из List по индексу использует- ся метод RemoveAt. ¢ Тип List объявляется с аргументом-типом в угловых скобках. Например, List<Frog> означает, что список List сможет хранить только объекты типа Frog. ¢ MетодContains проверяет, присутствует ли конкрет- ный объект в List. Метод IndexOf возвращает ин- декс конкретного элемента List. ¢ Свойство Count возвращает количество элементов в списке. ¢ Используйте индексатор (конструкция вида guys[3]) для обращения к элементу коллекции с заданным индексом. ¢ Для перебора List списка можно воспользоваться ци- клом foreach (по аналогии с массивами). ¢ List является обобщенной коллекцией; это означа- ет, что List может хранить любой тип. ¢ Все обобщенные коллекции реализуют обобщенный интерфейс ICollection<T>. ¢ <T> в определении обобщенного класса или интер- фейса заменяется типом при создании экземпляра. ¢ Используйте команду Go To Definition (Windows) или Go To Declaration (macOS) в Visual Studio для иссле- дования вашегокода идругихиспользуемых классов. foreach не может изменять свою коллекцию Не изменяйте коллекцию в процессе ее перебора в цикле foreach! Если коллекция будет изменена, выдается исключение InvalidOperationException. Выможете убедиться в этом сами. Создайте новое консольное приложение . N ET Core, затем добавьте код для создания нового списка List<string>, добавьте значение, вос - пользуйтесьциклом foreachдля перебора и добавьте новое значение в коллекцию внутри цикла. При запуске кода цикл foreach выдаст исключение. И помните: при использовании обобщенных классов всегда необходимо указывать тип — таким образом List<string> обозначает список строк. Будьте осторожны! КЛЮЧЕВЫЕ МОМЕНТЫ
дальше 4 463 перечисления и коллекции Развлечения с магнитами Сможете ливырасставить фрагментыкода, чтобы создать работоспособное консоль- ное приложение, которое выводит приве- денный результат на консоль? static void Main(string[] args) { } ze ro one three four 4.2 Резу ль тат } a.RemoveAt(2); List<string> a = new List<string>(); static void PppPppL (List<string> a){ a.A dd( zi lch ); a.Add(first); a.Add(second); a.Add(third); if (a.Contains("two")) { a.Add(twopointtwo); } if (a.Contains("three")) { a.Add("four"); } foreach (string element in a) { Console.WriteLine(element); } } if (a.IndexOf("four") != 4) { a.Add(fourth); } PppPppL(a); string zilch = "zero"; string first = "on e"; string second = "two"; string third = "three"; string fourth = "4.2 "; string twopointtwo = "2.2";
464 глава 8 static void Main(string[] args) { у списков есть тип Развлечения с магнитами Реш ен ие } a.RemoveAt(2); List<string> a = new List<string>(); static void PppPppL(List<string> a){ a.Add(zilch); a.A dd (fi rs t); a.Add(second); a. Add (th ir d); if (a.Contains("two")) { a.Add(twopointtwo); } if (a.Contains("three")) { a.Add("four"); } foreach (string element in a) { Console.WriteLine(element); } } if (a.IndexOf("four") != 4) { a.Add(fourth); } PppPppL(a); string zilch = "zero"; string first = "on e"; string second = "two"; string third = "three"; string fourth = "4.2 "; string twopointtwo = "2.2"; А вы сможете определить, по- чему значение "2.2" не добавляется в список, хотя оно объявляется здесь и передается Add ниже? Воспользуй- тесь отладчиком, чтобы разобрать- ся в происходящем! Метод RemoveAt удаляет элемент с индексом 2 — это третий эле- мент в списке. Цикл foreach перебирает все элементы списка и выводит их. Метод PppPppL ис- пользует цикл foreach для перебора списка строк, их объединения в одну большую стро- ку, которая выводится в окне сообщения. } Помните, как мы обсуждали использование содержательных имен в главе 3? Да, они улучшают код, но с ними эти упражнения получились бы слишком простыми. Только не используйте загадочные имена вида PppPppL в ре аль ных програ мма х! Если вы хотите выполнить этот код, не забудьте включить строку «using System. Collection.Generic» в начало. z ero one three four 4.2 Результат
дальше 4 465 перечисления и коллекции В: С чего бы мне использовать пере- числение вместо коллекции? Развеони не решают похожие задачи? О:Перечисления очень сильноотличаются от коллекций. Прежде всего перечисления являются типами, тогда как коллекции яв- ляются объектами. Перечисления можно рассматривать как удобный механизм хранения списков констант, к которым можно обращаться по имени. Они упрощают чтение вашего кода и гарантируют, что для обращения к часто используемым значениям всегда будут использоваться правильные имена. Вколлекции могут храниться практически любые данные, потому что в коллекции хранятся ссылки на объекты, по кото - рым можно обращаться к компонентам объектов. С другой стороны, перечисле- нию долженбыть присвоен один из типов значений C# (см. главу 4).Их можно пре- образовать в значения, но не в ссылки. Крометого, перечислениянемогутдинами- чески изменять свой размер. Они не могут реализовывать интерфейсыили содержать методы,и для сохранения значенияиз пере- числения в другой переменной придется преобразовать их кдругому типу.Сучетом всего сказанного мы получаем два сильно различающихся способа храненияданных. Каждый из них полезен по-своему. В: Похоже, класс List —довольно мощ- ная штука. Тогда зачем использовать массивы? О: Д ля х ранения кол лекции объ ектов обы ч - ноиспользуется список,а немассив . Однаиз ситуаций, в которых предпочтение отдается массивам (примеры будут приведены позд- нее в книге), — ч т ение пос ледовательности байтов(например,из файла).В этомслучае часто вызывается метод класса .NET, воз - вращающий byte[]. К счастью, списки легко преобразуютсяв массивы(вызовом метода ToArray), а массивы преобразуются в списки (припомощиперегруженногоконструктора). В: Я не понимаю, почему коллекции называются «обобщенными»? О: О бобщ енная колл екц ия пре дс тавл яет собой объект коллекции (или встроенный объект,который позволяет хранить наборы другихобъектов иуправлять ими), настроен- ный для хранения только одного типа (или нескольких типов, как вы вскореувидите). В:Ладно, с «коллекцией» понятно. Но что делает ее «обобщенной»? О:Когда-тов супермаркетах продавались обобщенные товары в белой упаковке с чернойнадписью,которая сообщалатолько видтовара(«Картофельныечипсы»,«Кола», «Мыло» и т. д.). То же самое происходит с обобщенными типами данных. СписокList<T> будет рабо- тать одинаково, какие бы данные в нем ни хранились. Список объектов Shoe, список объектовCard, int,long и даже другие спи- ски — все они могутдействовать науровне контейнера. Вы всегда можете добавлять, удалять, вставлять элементы и т . д ., к акие бы данные ни хранились в списке. В:Можноли создать списокбез типа? О: Нет. С каждым списком, а на самом деле с каждой обобщенной коллекцией (вскоре вы узнаете о других обобщенных коллекциях),долженбыть связан тип. ВC# поддерживаются необобщенные списки ArrayList, в которых могут храниться лю- быеобъекты. Если вы хотите использовать ArrayList,в программу необходимовключить строку using System.Collections;. Впрочем, делать это приходится нечасто, потому чтоList<Object> обычнохорошо подходит для ситуации, в которой можно было бы использовать нетипизованный объект ArrayList. При создании но- вого объекта List всегда указывается тип — он сообща- ет C# тип данных, которые будут хра- ниться в списке. В List могут хра- ниться как типы значений (int, bool, decimal и т. д.), так и классы. Массивы потребляют мень- ше памяти и процессорного времени в программах, но прирост производитель- ности очень невелик. Если вы выполняете одну и ту же операцию, скажем , миллио- ны раз в секунду, то лучше использовать массив вместо списка. Но если ваша про- грамма работает медленно, крайне маловероятно, что переход со списка на массив решит эту проблему. Термин «обобщенный» обо- значает тот факт, что хотя в конкретном экзем- пляре List могут храниться элементы одного конкрет- ного типа, сам класс List работает с любым типом. Именно на это указывает <T> в имени — эта часть сообщает, что список содер- жит набор ссылок типа T. часто Зада вае мы е вопросы
466 глава 8 инициализация коллекций Инициализаторы коллекций похожи на инициализаторы объектов List<Shoe> shoeCloset = new List<Shoe>(); shoeCloset.Add(new Shoe() { Style = Style.Sneakers, Color = "Black" }); shoeCloset.Add(new Shoe() { Style = Style.Clogs, Color = "Brown" }); shoeCloset.Add(new Shoe() { Style = Style.Wingtips, Color = "Black" }); shoeCloset.Add(new Shoe() { Style = Style.Loafers, Color = "White" }); shoeCloset.Add(new Shoe() { Style = Style.Loafers, Color = "Red" }); shoeCloset.Add(new Shoe() { Style = Style.Sneakers, Color = "Green" }); List<Shoe> shoeCloset = new List<Shoe>() { new Shoe() { Style = Style.Sneakers, Color = "Black" }, new Shoe() { Style = Style.Clogs, Color = "Brown" }, new Shoe() { Style = Style.Wingtips, Color = "Black" }, new Shoe() { Style = Style.Loafers, Color = "White" }, new Shoe() { Style = Style.Loafers, Color = "Red" }, new Shoe() { Style = Style.Sneakers, Color = "Green" }, }; C#предоставляет довольноудобную сокращенную запись дляситуаций, вкоторыхтребу- етсясоздать списокинемедленнозанестивнегонабор элементов.Присозданиинового объектаList можно воспользоваться инициализатором коллекции для определения на- чального списка элементов. Элементыбудутдобавлены сразужепослесоздания списка. Т о т ж е к о д , п е р е п и с а н н ы й с и н и ц и а л и з а т о р о м к о л л е к ц и и Этот код создает новый экзем- пляр List<Shoe> и заполняет его объектами Shoe, для чего много- кратно вызывается метод Add. За командой созда- ния списка следуют фигурные скобки с отдельными коман- дами «new», разде- ленными запятыми. Вы не ограничены использованием команд «new» в инициализа- торе — также можно включать сюда переменные. Инициализатор коллекции делает код более компакт- ным, так как он позволяет объединить операцию созда- ния списка с добавлением исходного набора элементов. Чтобы создать инициали- затор коллекции, возьмите каждый элемент, который добавлялся вызовом Add, и включите его в команду создания списка. А вы заметили, что каждый объект Show инициализиру- ется собственным иници- ализатором объекта? Да, инициализаторы объектов могут вкладываться в иници- ализатор коллекции.
дальше 4 467 перечисления и коллекции D uck S ize Kind Создание списка уток Нижеприведен класс Duck, в котором хранится информация об утках, живущих с вами по соседству. Создайтеновый проект консольного приложения ивключитевнего классDuck ипере- числение KindOfDuck. Для каждой утки хранит- ся ее размер (в дюймах). Также встреча- ются мускусные утки (Muscovy). Некоторые из уток являются кряквами (Mallard). class Duck { public int Size { get; set; } public KindOfDuck Kind { get; set; } } enum KindOfDuck { Mallard, Muscovy, Loon, } Инициализатор для списка уток В коллекции изначально хранятся данные шести уток, поэтому мы создадимсписок List<Duck> синициализатором коллекциииз шести команд.Каждаякомандав инициализаторе создает новый объектDuck, используяинициализатор объекта дляопределения значенийполей Sizeи Kind объекта Duck. Убедитесь в том, что в начале Program.cs располагается соответствующая директива using: using System.Collections.Generic; Затем добавьте следующийметод PrintDucks вклассProgram: public static void PrintDucks(List<Duck> ducks) { foreach (Duck duck in ducks) { Console.WriteLine($"{duck.Size} inch {duck.Kind}"); } } Наконец,добавьте следующийкодвметодMainизфайлаProgram.cs . Код создает списокListс объектамиDuckи выводит их: List<Duck> ducks = new List<Duck>() { new Duck() { Kind = KindOfDuck.Mallard, Size = 17 }, new Duck() { Kind = KindOfDuck.Muscovy, Size = 18 }, new Duck() { Kind = KindOfDuck.Loon, Size = 14 }, new Duck() { Kind = KindOfDuck.Muscovy, Size = 11 }, new Duck() { Kind = KindOfDuck.Mallard, Size = 14 }, new Duck() { Kind = KindOfDuck.Loon, Size = 13 }, }; PrintDucks(ducks); Добавьте Duck и KindOfDuck в проект. Перечисление KindOfDuck используется для отслеживания пород уток, поддерживаемых в приложе- нии. Обратите внимание на то, что значения элементам перечисления не присваива- ются — это очень типич- но. Числовые значения нас не интересуют, и значения по умолчанию (0, 1 , 2 ...) пре- красно подойдут. Выполните свой код — он выводит на консоль набор объектов Duck. Сделайте это!
468 глава 8 Списки удобны, но с СОРТИРОВКОЙ могут возникнуть проблемы Нетрудно представить различные способы сортировки чисел или строк. Но как от- сортировать два объекта, особенноеслионисодержатнесколькополей?Вкаких-то случаях можно упорядочить объекты по значению поля Name, тогда как в другихслучаях имеет смысл упорядочить объекты на основанииполяразмера или даты рождения. Существует множество способовупорядоченияобъектов, ивсеониподдерживаются списками. Список уток можно отсортировать по размеру... . ..или по породе. По породе... сортировкауток Списки умеют сортировать свое содержимое КаждыйсписокListсодержит методSort, которыйпереставляет всеэлементы спискав некотором порядке.Спискиужеумеют сор- тировать многиевстроенные типыи классы,ивыможетелегко обучить их сортировать классы, написанныевами. О б ъ е к т D u c k О б ъ е к т D u c k О б ъ е к т L i s t < D u c k > О б ъ е к т D u c k 17” d uck 11” d uck 14” du ck Sort() О б ъ е к т L i s t < D u c k > О б ъ е к т D u c k О б ъ е к т D u c k 11” d uck О б ъ е к т D u c k 14” du ck 17” du ck Пос ле того как список уток будет отсортирован, он содержит те же эле- менты, но в другом порядке. Формально сортировкой зани- мается не объект List<T>. Это задача объекта IComparer<T>, о котором вы вскоре узнаете. От маленьких к большим...
дальше 4 469 перечисления и коллекции IComparable<Duck> помогает списку List сортировать объекты Duck Еслиувас имеетсясписокList счисламиивы вызываетеего методSort, список будет отсортированот меньшихчиселкбольшим.КаксписокListопределит,как нужно сортировать объектыDuck?Мы сообщаем List.Sort,чтообъектыкласса Duckмогутучаствовать всортировке,иделаетсяэто так,какмыобычноуказы- ваем, что класс может выполнятьопределеннуюзадачу: припомощиинтерфейса. Метод List.Sort умеет сортировать любые типы или классы, реализующие интерфейсIComparable<T>. Интерфейс содержит всего один компонент — методCompareTo. Sort использует методCompareToобъекта длясравненияего с другимиобъектамииповозвращенномузначению(int)определяет,какойиз объектовдолженпредшествовать другому. Метод CompareTo объекта сравнивает его с другим объектом Чтобынаделить объектListвозможностьюсортироватьуток,можно изменить класс Duck иреализоватьIComparable<Duck>, т. е. добавить его единствен- ныйкомпонент— методCompareTo,получающийссылку наDuckвпараметре. ОбновитеклассDuckиреализуйтеинтерфейсIComparable<Duck>,чтобысор- тировка производилась по размеру: class Duck : IComparable<Duck> { public int Size { get; set; } public KindOfDuck Kind { get; set; } public int CompareTo(Duck duckToCompare) { if (this.Size > duckToCompare.Size) return 1; else if (this.Size < duckToCompare.Size) return -1; else return 0; } } Можно заставить любой класс ра- ботать со встроен- ным методом Sort класса List — для этого следует ре- ализовать в нем IComparable<T> и добавить метод CompareTo. Тип сравниваемых объектов зада- ется при реализации интерфейса IComparable<T>. Многие методы CompareTo похожи на этот. Метод сначала сравнивает поле Size с полем Size другого объ- екта Duck. Если у текущего объек- та Duck оно больше, возвращается 1; если меньше, воз- вращается –1 . Если значения равны, то возвращается 0. Если вы хотите отсортировать список от меньших значений к большим, метод CompareTo должен возвращать положительное зна- чение при сравнении с меньшим объек- том и отрицатель- ное — при сравнении с большим. Добавьте следующую строку в конец метода Main, непосредственно перед вызовом PrintDucks. Она приказываетспискуутокотсортировать себя.Теперьуткиупорядочиваютсяпо размерупередвыводом на консоль ducks.Sort(); PrintDucks(ducks); Если объект, с которым сравнивается текущий объект, должен следовать после текущего в от- сортированном списке, метод CompareTo дол- жен возвращать положительное число. Если он долженпредшествовать текущему объекту, метод CompareTo должен возвращать отрицательное число. Если они равны, метод возвращает нуль.
470 глава 8 Использование IComparer для определения порядка сортировки КлассDuckреализуетIComparable,такчто ListSort знает,как отсортировать списокListобъектовDuck. Но что, если вы захотите отсортировать их способом, отличным от стандартного?Или если потребу- ется отсортировать объекты типа,не реализующего IComparable?В таком случае в аргументеList.Sort можно передать объект-компаратордля определения другогоспособа сортировкиобъектов.Обратите внимание на перегрузку List.Sort: ПерегруженнаяверсияList.Sort получает ссылку на IComparer<T>, где T заменяется обобщенным типом вашего списка (таким образом, для List<Duck> передается аргументIComparer<Duck>, для List<string> — IComparer<string>, ит. д.) .Таккакпередаетсяссылка наобъект,реализующийинтерфейс, мы знаем, что это означает: что объектвыполняетконкретную задачу.В данном случаезадачей являетсясравнениепар элементов списка иопределение порядка их сортировки для List.Sort. ИнтерфейсIComparer<T> содержит только один компонент: метод с именем Compare. Этот метод практически не отличается от метода CompareTo вIComparable<T>: онполучает два параметра-объек- та x и y ивозвращаетположительноезначение,еслиx предшествуетy;отрицательноезначение,еслиx следуетпослеy;илинуль,если ониравны. Включение IComparer в проект Добавьте классDuckComparerBySize в проект. Это объект-компаратор, которыйбудет передаваться впараметреList.Sortдлятого,чтобыуткисортировались поразмеру. ИнтерфейсIComparer принадлежит пространствуименSystem.Collections.Generic, поэтому придобавле- ниикласса в новыйфайл следуетпроследить за тем,чтобы он содержал необходимую директиву using: using System.Collections.Generic; Кодкласса компаратора выглядит так: class DuckComparer BySize : IComparer<D uck> { public int Co mpare(Duck x, Duck y ) { if (x.Siz e < y.Size) retur n -1; if (x.Siz e > y.Size) retur n 1; return 0; } } А вы сможете изменить DuckComparerBySize, чтобы утки сортировались поубыванию размера, т. е . от больших к меньшим? Если Compare возвращает отрицательное число, это означает, что объ ект x в порядке сортировки должен предшествовать объекту y. Иначе говоря, x «меньше» y. Любое положительное значение означает, что объект x должен следовать за объектом y, т. е . x «больше» y. Нуль означает, что два значения «равны» . сортировка по нестандартному критерию Объект-компаратор пред- ставляет собой экземпляр кл асса, р еали зую щег о интерфейс IComparer<T>, который можно передать в ссылке List.Sort. Метод Compare работат так же, как метод CompareTo ин- терфейсаIComparable<T>. Когда метод List.Sort сравнивает элементы для сортировки, он передает пары объектов методу Compare объекта-компа- ратора, так что список List будет сортироваться по- разному в зависимости от того, как вы реализовали компаратор.
дальше 4 471 перечисления и коллекции Создание экземпляра компаратора Если вы хотите выполнить сортировку с использованием IComparer<T>, не - обходимо создать новый экземпляр класса, реализующего этот интерфейс, в данном случаеDuck. Это объект-компаратор, который помогает List.Sort определить порядоксортировкиэлементов. Как ис любым другим(нестати- ческим) классом, передиспользованиемнеобходимосоздать его экземпляр: Несколько реализаций IComparer, несколько способов сортировки объектов Вы можетесоздать несколько классов IComparer<Duck> с разной логикойсортировки, чтобысортироватьсписокутокразнымиспосо- бами. Далее остаетсялишь воспользоватьсянужным компаратором, когда потребуется отсортировать список каким-то определенным образом.Нижеприведена ещеодна реализация компаратораDuck, которую следует добавить в проект cl ass DuckComparer ByKind : ICompar er<Duck> { public int Comp are(Duck x, Duck y) { if (x.Kind < y.Kind) return - 1; if (x.Kind > y.Kind) return 1 ; else return 0 ; } } Мы сравнили свойства Kind объектов Duck, так что объ- екты будут отсортированы по значению индекса свойства Kind. Этот компаратор сортирует уток по породе. Не забывайте, что при сравнении значений из пере- числения Kind вы сравниваете их индексы в перечислении. Мы не при- сваивали конкретные значения при объявлении перечисления KindOfDuck, поэтому им присваиваются зна- чения0,1,2ит.д.впорядкеих следования в объявлении перечис- ления (Mallard соответствует 0, Muscovy — 1, Loon — 2). Обратите внимание: поня- тия «больше» и «меньше» здесь имеют другой смысл. Мы использовали < и > для сравнения значений индексов перечисления, что позволя- ет нам разместить уток в нужном порядке. При м ер сов м естного ис- пользования перечислений и списков. Перечисления за м еня ю т обычные числа и использу ю тся при сорти- ровке списков. ICom parer <Duck> size Compa rer = new DuckC ompar erBySi ze(); duc ks.So rt(si zeCom parer) ; Pri ntDuc ks(duc ks); Методу Sort в параметре передается ссылка на новый объект DuckComparerBySize. Замените ducks.Sort в методе Main двумя строками кода. Список по-прежнему сорти- руется, но теперь для сортировки использу- ется компаратор. Изменитесвою программу, чтобы в ней использовался компаратор. Следующийфрагментсортирует объектыDuck передвыводомна консоль. ICom parer <Duck> kindComparer = new DuckCompa rerByKind(); ducks.So rt(kindComparer) ; Pri ntDuck s(duc ks);
472 глава 8 а теперь займемся сортировкой карт Компараторы могут выполнять сложные сравнения Усоздания отдельного класса для сортировкиобъектовDuckесть одно важноепреимущество: вы може- тевстроить вклассболеесложную логику — идобавить компоненты,которыепомогутопределить, как именно должен быть отсортировансписок. enum SortCriteria { SizeThenKind, KindThenSize, } class DuckComparer : IComparer<Duck> { public SortCriteria SortBy = SortCriteria.SizeThenKind; public int Compare(Duck x, Duck y) { if (SortBy == SortCriteria.SizeThenKind) if (x.Size > y.Size) return 1; else if (x.Size < y.Size) return -1; els e if (x.Kind > y.Kind) return 1; else if (x.Kind < y.Kind) return -1; e lse return 0; el se if (x.Kind > y.Kind) return 1; else if (x.Kind < y.Kind) return -1; els e if (x.Size > y.Size) return 1; else if (x.Size < y.Size) return -1; e lse return 0; } } DuckComparer comparer = new DuckComparer(); Console.WriteLine("\nSorting by kind then size\n"); comparer.SortBy = SortCriteria.KindThenSize; ducks.Sort(comparer); PrintDucks(ducks); Console.WriteLine("\nSorting by size then kind\n"); comparer.SortBy = SortCriteria.SizeThenKind; ducks.Sort(comparer); PrintDucks(ducks); Более сложный класс для сравнения Duck. Его метод Compare получает те же па- раметры, но для определения способа сортировки он обра- щается к открытому полю SortBy. Перечисление сообщает объ- екту-компаратору, каким способом следует отсорти- ровать объекты Duck. Команда if проверяет поле SortBy. Если поле содержит SizeThenKind, то утки снача- ла сортируются по размеру, а при одинаковых размерах — по породе. Вместо того чтобы просто вер- нуть 0 при совпадении размеров, компаратор проверяет породу и возвращает 0 только в том слу- чае, если у двух уток совпадает как размер, так и порода. Если SortBy не содержит SizeThenKind, компаратор сначала выполняет сортировку по породе. Если у двух уток порода совпадает, тогда компаратор сравнивает их размеры. Добавьте этот код в конец методаMain. В нем использует- ся объект-компаратор, а перед вызовом ducks.Sort задается значение поля SortBy. Теперь можно изменить способ сорти- ровки списков своих объектов простым изменением свойства в ко мп арато ре.
дальше 4 473 перечисления и коллекции Постройте консольное приложение, которое создает список карт в случай- ном порядке, выводит их на консоль, использует объект-компаратор для со- ртировки карт, а затем выводит отсортированный список. Напишите метод для создания перетасованного набора карт. Создайте новое консольное приложение. Добавьте перечисление Suits, перечисление Values икласс Card,приведенныйранеевэтой главе.Затем добавьтевProgram.cs два ста- тическихметода:методRandomCard, возвращающийссылкуна карту со случайноймастью и номиналом, и PrintCards, который выводит List<Card>. Создайте класс, реализующий IComparer<Card> для сортировки карт. Вам предоставляется хорошая возможность воспользоваться меню Quick Actions среды IDE для реализации интерфейса. Добавьте класс с именем CardComparerByValue, а затем реализуйте интерфейс IComparer<Card>: class CardComparerByValue : IComparer<Card> Щелкнитена IComparer<Card> и наведите указатель мыши наI. На экране появляется значок с изображениемлампочки( или ).Если щелкнуть на значке, IDE открывает меню QuickActions: В IDE существует удобный способ ускоренного вызо- ва меню Quick Actions: на- жмите Ctrl+ (Windows) или Option+Enter (Mac). Выберитекоманду «Implement interface» — она приказываетIDEавтоматически заполнить все методы и свойства интерфейса, которые необходимо реализовать. В данном случае создаетсяпустойметодCompareдлясравнениядвухкарт, xи y. Метод долженвозвращать 1,еслиx большеy; –1,еслиx мень- шеy;0,если они равны. Сначала картыупорядочиваются по масти:первымиидутбубны(Diamonds),затем трефы(Clubs), червы (Hearts) и пики(Spades).Проследите за тем, чтобы любойкороль следовалпослелюбоговалета, который, всвою очередь,следует после любойчетверки,котораяследует после любого туза. Значения перечислений можно сравнивать без приведения типа: if (x.Suit< y.Suit). Убедитесь в том, что программа выдает правильный результат. Напишите метод Main, чтобы вывод выглядел так: ‘ Программа запрашивает количество карт. ‘ Еслипользователь вводитдопустимоечисло и нажимает Enter, программа генерируетсписокслучайныхкарт ивы- водитих. ‘ Список карт сортируется с использованием компаратора. ‘ Программа выводитотсортированныйсписок карт. 1 2 3 Enter number of ca rds: 9 Eight of Spades Nine of Hearts Four o f Hearts Nine of Hearts King of Diamon ds King of Spades Six of Spades Seven o f Clubs Seven o f Clubs ... sortin g the card s ... King of Diamon ds Seven o f Clubs Seven o f Clubs Four o f Hearts Nine of Hearts Nine of Hearts Six of Spades Eight of Spades King of Spades Упражнение Ваш объект IComparer должен сортировать карты по номиналу, чтобы карты с малыми номиналами располага- лись в начале списка.
474 глава 8 Постройте консольное приложение, которое создает список карт в случайном порядке, вы- водит их на консоль, использует объект-компаратор для сортировки карт, а затем выводит отсортированный список. Не забудьте добавить директиву using System.Collections. Generis; в начало файла с точкой входа. class CardComparerByValue : IComparer<Card> { public int Compare(Card x, Card y) { if (x.Suit < y.Suit) return -1; if (x.Suit > y.Suit) return 1; if (x.Value < y.Value) return -1; if (x.Value > y.Value) return 1; return 0; } } class Program { private static readonly Random random = new Random(); static Card RandomCard() { return new Card((Values)random.Next(1, 14), (Suits)random.Next(4)); } static void PrintCards(List<Card> cards) { foreach (Card card in cards) { Console.WriteLine(card.Name); } } static void Main(string[] args) { List<Card> cards = new List<Card>(); Console.Write("Enter number of cards: "); if (int.TryParse(Console.ReadLine(), out int numberOfCards)) for (int i = 0; i < numberOfCards; i++) cards.Add(RandomCard()); PrintCards(cards); cards.Sort(new CardComparerByValue()); Console.WriteLine("\n... sorting the cards ...\n"); PrintCards(cards); } } Внутренняя реализация сортировки карт использует встроенный ме- тод List.Sort. Sort получает объект IComparer, который содержит всего один метод: Compare. Такая реализация получает две карты и сначала сравни- вает их номиналы, а потом масти. Если ни одна из четырех других команд не сработает, карты должны быть одина- ковыми — вернуть нуль. Мы хотим, чтобы все бубны (Diamonds) предшество- вали всем трефам (Clubs), поэтому начинать нужно со сравнения мастей. При этом стоит воспользо- ваться перечислениями. Эти команды выполняются толь- ковтомслучае,еслиxиyиме- ют одинаковые значения, а сле - довательно, первые две команды return не были выполнены. Здесь создается обобщенный список List объектов Card для хранения карт. Когда карты окажутся в списке, их можно легко отсортировать с ис- пользованием IComparer. как заставить каждый объект описать себя Мы опусти- ли фигурные скобки. Как вы считаете, это упрощает или усложняет чтение кода? Упражнение Решение
дальше 4 475 перечисления и коллекции Каждый объект содержит методToString, которыйпреобразуетегов строку.Насамомделе выуже поль- зовались им— каждыйраз, когда выиспользовали {фигурныескобки}при строковой интерполяции, при этом вызывалсяметод ToStringдлясодержимогофигурныхскобок.IDEтожеиспользует ToString().Когда вы создаетекласс, он наследует методToStringотObject—базовогокласса верхнегоуровня,расширяемого всеми остальными классами. МетодObject.ToStringвыводитполноеимякласса,т. е. пространство имен,за которымследует точкаи имя класса. Таккакмы использовали пространство именDucksProject при написании этой главы, классу Duck будет соответствовать полное имя DucksProject.Duck: Console.WriteLine(new Duck().ToString()); " DucksProject.Duck" IDEтакжевызываетметодToString —например, припросмотреилиотслеживании значений переменных: IDE вызывает метод ToString при просмотре или отслеживании переменной, но метод ToString, уна- следованный Duck от Object, просто возвращает имя класса. В данном случае та- кое «описание» бесполезно. Хм-м, информация оказалась менее полезной, чем можно было надеяться. Мы видим, что список со- держит шесть объектовDuck.РаскрывузелDuck,вы увидите его значения Kind иSize. Но нелучше ли увидеть вс е да нные нем едленно ? Переопределение метода ToString для просмотра объектов Duck в IDE Ксчастью,ToString является виртуальным методом Object,базового класса всех объектов.Следователь- но,вам достаточно переопределить метод ToString,а когдаэто будетсделано,результаты немедленно появятсяв окнеWatchвIDE! Щелкнитена toString(), чтобы приказатьIDE добавить новыйметод ToString.Изменитеего код, чтобыонвыгляделтак: public override string ToString() { return $"A {Size} inch {Kind}"; } Запустите программу и снова просмотрите список. Теперь вIDE вы- водится содержимое вашихобъектовDuck. Когда отладчик IDE выводит содержи- мое объекта, он при этом вызывает метод ToString объекта. Переопределение метода ToString позволяет объекту описать себя Когда вы наводите указатель мыши на «ducks», IDE выводит содержимое списка List, как это делалось ранее с массивами.
476 глава 8 цикл foreach Обновите циклы foreach, чтобы объекты Duck и Card выводили свои описания на консоль Выужевидели два примера перебора списка объектов вцикле и вызова Console.WriteLine для вывода на консоль строкидля каждого объекта. Вспомните цикл foreach, который выводил всеобъектыCard из коллекции List<Card>: foreach (Card card in cards) { Console.WriteLine(card.Name); } МетодPrintDucks делалнечто подобноес объектамиDuckвList: foreach (Duck duck in ducks) { Console.WriteLine($"{duck.Size} inch {duck.Kind}"); } Этооперациядовольночасто встречаетсяприработе собъектами.Теперь, когдавашобъектDuckсодержит методToString, методPrintDucks должен воспользоватьсяэтим обстоятельством. Используйтефункцию IDEIntelliSense дляпросмотра перегруженныхверсий метода Console.WriteLine, а конкретно этой: Если передать Console.WriteLine любой объект, будет вызван его метод ToString. Замените метод PrintDucks следующей реализацией, в которой вызывается перегруженная версия: public static void PrintDucks(List<Duck> ducks) { foreach (Duck duck in ducks) { Console.WriteLine(duck); } } Заменитеметод PrintDucks этим методом и снова запустите код. Программа выдаст тотжерезультат. Если,допустим,вы захотитедобавить вDuckсвойствоColorилиWeight,будетдостаточно обновить ме- тодToString,иэто изменениеотразитсяна всем,что использует этот метод(включая методPrintDucks). Добавьте метод ToString в объект Card ВашобъектCardуже содержитсвойствоName,возвращающее имя карты: public string Name { get { return $"{Value} of {Suit}"; } } Новедь именно этодолженделать метод ToString.Добавьте метод ToString в класс Card: public override string ToString() { return Name; } Это упростит отладку программ, в которых используются объектыCard. Если передать Console. WriteLine ссылку на объ- ект, метод автома- тически вызовет метод ToString этого объекта. Мы решили обращаться к свойству Name из метода ToString. Как вы оцениваете этот выбор? Не лучше было бы удалить свойство Name и переместить его код в метод ToString? Когда вы возвращае- тесь к изменению своего кода, часто при- ходится принимать подобные решения, и не всегда очевидно, какой выбор лучше.
дальше 4 477 перечисления и коллекции Прочитайте этот код и напишите, какой результат он будет выдавать. enum Breeds { Collie = 3, Corgi = -9, Dachshund = 7 , Pug=0, } class Dog : IComparable<Dog> { public Breeds Breed { get; set; } public string Name { get; set; } public int CompareTo(Dog other) { if (Breed > other.Breed) return -1; if (Breed < other.Breed) return 1; return -Name.CompareTo(other.Name); } public override string ToString() { return $"A {Breed} named {Name}"; } } class Program { static void Main(string[] args) { List<Dog> dogs = new List<Dog>() { new Dog() { Breed = Breeds.Dachshund, Name = "Franz" }, new Dog() { Breed = Breeds.Collie, Name = "Petunia" }, new Dog() { Breed = Breeds.Pug, Name = "Porkchop" }, new Dog() { Breed = Breeds.Dachshund, Name = "Brunhilda" }, new Dog() { Breed = Breeds.Collie, Name = "Zippy" }, new Dog() { Breed = Breeds.Corgi, Name = "Carrie" }, }; dogs.Sort(); foreach (Dog dog in dogs) Console.WriteLine(dog); } } Приложение вы- водит шесть строк на кон- соль. Сможете ли вы заранее определить, что она выве- дет, и записать результаты? Попробуйте определить результат ис- ключительно на осн ова нии чтения кода, без запуска прило- жения. Подсказка: обра- тите внимание на минус! Возьми в руку карандаш
478 глава 8 подробнее о циклахforeach ¢ Инициализаторы коллекций задают содержимое List<T> или другой коллекции при создании. Спи- сок объектов, разделенных запятыми, указывается в угловых скобках. ¢ Инициализатор коллекции делает ваш код более компактным: он позволяет объединить создание списка с добавлением исходного набора элементов (хотякод быстрее работать небудет). ¢ Метод List.Sort сортирует содержимое коллекции, изменяя порядок содержащихся в ней элементов. ¢ Интерфейс IComparable<T> содержит один метод CompareTo, который используется List.Sort для опре- деления порядка сортируемых объектов. ¢ Перегруженный метод представляет собой метод, который может вызываться разными способами с разными конфигурациями параметров. В окне IDE IntelliSense можно просмотреть разные перегружен- ные версии метода. ¢ Метод Sort имеет перегруженную версию, получаю- щую объект IComparer<T>, который затем использу- ется для сортировки. ¢ IComparable.CompareTo и IComparer.Compare срав- нивают пары объектов. Они возвращают –1 , если первый объект меньше второго; 1, если первый объ- ект больше второго; или 0, если они равны. ¢ Класс String реализует IComparable. Интерфейс IComparer или IComparable для класса, включающего строковый компонент, может вызвать метод Compare или CompareTo для определения порядка сортировки. ¢ Каждый объект содержит метод ToString, который преобразует его в строку.Метод ToString вызывается каждый раз, когда вы используете строковую интер- поляцию или конкатенацию. ¢ Реализация ToString по умолчанию наследуется от Object.Она возвращает полное имя класса, которое состоит из пространства имен, точки и имени класса . ¢ Переопределите метод ToString, чтобы при интер- поляции, конкатенации и многих других операциях использовалось нестандартное строковое представ- ление класса. Ниже приведен результат выполнения приложения. А вы правильно за- писали его? Если вы ошиблись — ничего страшного! Просто вернитесь и еще раз присмотритесь к перечислению. • Вы заметили, что элементы имеют разные значения? • СвойствоName представляет собой строку, а строки такжереализуют IComparable, поэтому мы можем вызвать метод CompareTo для их сравнения. • Также повнимательнее присмотритесь к методу CompareTo: вы заметили, что он возвращает –1 , если другая породабольше;1, если другаяпорода меньше, или -Name.CompareTo(other.Name)(обратите внимание на знак «минус»)? Сначала сортировка выполняется по породе (Breed), затем по кличке (Name), но сортировка как по Breed, так и по Name осуществляется в обратном порядке. Результат: A Dachshund named Franz A Dachshund named Brunhilda A Collie named Zippy A Collie named Petunia A Pug named Porkchop A Corgi named Carrie Когда метод CompareTo ис- пользует < и > для сравнения значений Breed, он использу- ет значения int из перечисления Breed, так что Collie соот- ветствует 3, Corgi соответствует –9, Daschund соответ- ствует 7, а Pug со- ответствует 0. КЛЮЧЕВЫЕ МОМЕНТЫ Возьми в руку карандаш Решение
дальше 4 479 перечисления и коллекции Поближепознакомимся сциклами foreach.ОткройтеIDE,найдитепеременнуюList<Duck>иис- пользуйтеIntelliSense для просмотра информации о методе GetEnumerator. Начните вводить «GetEnumerator» и посмотрите,что произойдет: Создайте массивArray[Duck] исделайте то же самое — массив тоже содержит массивGetEnumerator. Дело в том, что списки, массивы и другие коллекции реализуют интерфейс IEnumerable<T>. Вы уже знаете, что суть интерфейсов — поддержка однойфункцио- нальности разными объектами. Если объектреализует интерфейс IEnumerable<T>, то этой функциональностью является поддержка переборавнеобобщенныхколлекциях— иначе говоря, онпозволяет писать код,который перебирает содержимое этих коллекций.А конкретно это означает, что коллекция может использоваться в цикле foreach. Какжеэтовыглядит сточки зрениявнутреннегоустройства?Воспользуй- тесь командой GoToDefinition/Declarationc List<Duck>, чтобыувидеть реализуемые интерфейсы, как это делалось ранее. Затем сделайте то жесамое дляпросмотракомпонентовIEnumerable<T>. Чтовы видите? ИнтерфейсIEnumerable<T> содержит один компонент: метод GetEnumerator, который возвращает объект Enumerator. Объект Enumerator предоставляет механизмы перебора списка по порядку. Допустим, вы написали следующий цикл foreach: foreach (Duck duck in ducks) { Console.WriteLine(duck); } А вот какбудет работать цикл во внутреннейреализации: IEnumerator<Duck> enumerator = ducks.GetEnumerator(); while (enumerator.MoveNext()) { Duck duck = enumerator.Current; Console.WriteLine(duck); } if (enumerator is IDisposable disposable) disposable.Dispose(); Оба цикла выводятна консоль одниите жеобъектыDuck.Запуститеихиубедитесь втом,что вы- водимые результаты совпадают.(Ипоканеобращайтевниманияна последнюю строку;интерфейс IDisposableбудет рассматриваться вглаве10.) Когда вы перебираете содержимое списка или массива (или любой другой коллекции), метод MoveNext возвращает true, если в коллекции присутствует следующий элемент, или false, если перебор достигконца. СвойствоCurrent всегда возвращает ссылку на текущий элемент. Сложите все вместе — ивы получитециклforeach. Циклы foreach под увеличительным стеклом Если коллек- ция реализует IEnumerable<T>, это дает воз- можность ис- пользовать цикл, последовательно перебирающий ее содержимое. С т р о г о г о в о р я , к о д а б у д е т н е - м н о г о б о л ь ш е , н о и э т о т ф р а г - м е н т д а е т о б щ е е п р е д с т а в л е н и е о п р о и с х о д я щ е м .
480 глава 8 включение уток в список пингвинов Повышающее приведение типа всего списка с использованием IEnumerable<T> Помните, что любой объект можно повысить до его суперкласса? Еслиу вас имеется список List объектов, вы можете выполнить повышающее приведение типа для всего списка. Этот механизм на- зывается ковариантностью, и все, что необходимо для его использования, — ссылка на интерфейс IEnumerable<T>. Посмотрим,как это работает. Начнем с классаDuck, с которым выужеработали вэтой главе. Затем мыдобавим классBird,которыйонрасширяет. КлассBird включаетстатический метод для перебора коллекции объектовBird.Можно лизаставитьегоработатьсо спискомList объектовDuck? Bird Na me Fly staticFlyAway D uck Siz e Kin d Na me Создайте новый проект консольного приложения. Добавьте базовый класс Bird (расширяемый классомDuck)иклассPenguin. Мы воспользуемся методом ToString,чтобыбыло лучше видно,какой класс что делает. class Bird { public string Name { get; set; } public virtual void Fly(string destination) { Console.WriteLine($"{this} is flying to {destination}"); } public override string ToString() { return $"A bird named {Name}"; } public static void FlyAway(List<Bird> flock, string destination) { foreach (Bird bird in flock) { bird.Fly(destination); } } } 1 Класс Bird, который рас- ширяется классом Duck. Вы изменяете объявление, чтобы класс расширял Bird, но остальной код остает- ся неизменным. Затем оба класса добавляются в кон- сольное приложение, чтобы вы могли поэксперименти- ровать с ковариантностью. Так как все объекты Duck являются объектами Bird, ко- вариантность позволяет пре- образовать коллекцию Duck в коллекцию Bird. Это может быть очень полезно, если вам приходится передавать List<Duck> методу, который принимает только List<Bird>. Статический метод FlyAway работает с коллекцией объ- ектов Bird. Но что, если вы хотите передать ему список List объектов Duck? Ковариантность пр едст авл яет соб ой механ из м неяв но го преобразования ссылки на субкласс в ссылку на супер- класс в C#. Термин «неявный» означает лишь то, что C# может определить, как выполнить пре- образование, без явного приведения типа разработчиком. Сделайте это!
дальше 4 481 перечисления и коллекции Добавьте класс Duck в приложение. Измените объявление, чтобы классрасширял Bird. Такженеобходимо добавить перечислениеKindOfDuck,приведенноеранее вэтойглаве: class Duck : Bird { public int Size { get; set; } public KindOfDuck Kind { get; set; } public override string ToString() { return $"A {Size} inch {Kind}"; } } Создайте коллекцию List<Duck>. Добавьте следующий фрагментв метод Main — это код, приведенныйранеевэтой главе, и еще одна строка дляповышающегоприведениятипа к List<Bird>: List<Duck> ducks = new List<Duck>() { new Duck() { Kind = KindOfDuck.Mallard, Size = 17 }, new Duck() { Kind = KindOfDuck.Muscovy, Size = 18 }, new Duck() { Kind = KindOfDuck.Loon, Size = 14 }, new Duck() { Kind = KindOfDuck.Muscovy, Size = 11 }, new Duck() { Kind = KindOfDuck.Mallard, Size = 14 }, new Duck() { Kind = KindOfDuck.Loon, Size = 13 }, }; Bird.FlyAway(ducks, "Minnesota"); Кодне компилируется!В сообщенииобошибке говоритсяо том, что коллекция Duckне может быть преобразована вколлекцию Bird. Попробуйте присвоить ducks списку List<Bird>: List<Bird> upcastDucks = ducks; Неработает. Мы получаем другую ошибку, в которой говорится о невозможности преобразо- ваниятипа: Разумно—происходящее в точности повторяет ситуацию сбезопаснымповышающими понижаю- щимпреобразованием, окоторой говорилось в главе6:мыможем использовать присваивание для понижающегопреобразования, нодлябезопасногоповышающегопреобразованиянеобходимо использоватьключевое словоis.Как жевыполнитьбезопасное повышениеList<Duck>вList<Bird>? Используйте ковариантность для преобразования. На помощь приходит ковариантность: вы можете воспользоваться присваиванием для повышающего преобразованияList<Duck> в IEnumerable<Bird>. После получения IEnumerable<Bird> можно вызвать его метод ToList для преобразования вList<Bird>. Для этого вначалофайла необходимо добавить директивыusing System. Collections.Generic; и using System.Linq;. IEnumerable<Bird> upcastDucks = ducks; Bird.FlyAway(upcastDucks.ToList(), "Minnesota"); 2 3 4 Скопируйте ини- циализатор кол- лекции, который использовался ранее, для инициализации списка List объек- тов Duc k. Мы изменили объявление, чтобы класс Duck расширял Bird. Остальной код Duck полностью совпадает с кодом предыду- щего проекта. enum KindOfDuck { Mallard, Muscovy, Loon, } Свойство Kind класса Duck исполь- зует перечисление KindOfDuck, поэто- му его тоже необхо- димо добавить. Коллекция ссылок на Duck была преобразована в коллекцию ссылок на Bird.
482 глава 8 Рассмотрим пример практического использования словаря. Небольшое консольное приложение ис- пользуетDictionary<string, string> для отслеживания любимой еды нескольких друзей: using System.Collections.Generic; class Program { static void Main(string[] args) { Dictionary<string, string> favoriteFoods = new Dictionary<string, string>(); favoriteFoods["Alex"] = "hot dogs"; favoriteFoods["A'ja"] = "pizza"; favoriteFoods["Jules"] = "falafel"; favoriteFoods["Naima"] = "spaghetti"; string name; while ((name = Console.ReadLine()) != "") { if (favoriteFoods.ContainsKey(name)) Console.WriteLine($"{name}'s favorite food is {favoriteFoods[name]}"); els e Console.WriteLine($"I don't know {name}'s favorite food"); } } } ключи и значения Использование Dictionary для хранения ключей и значений Списокнапоминает длиннуюстраницу, заполненную именами.А теперь представьте,чтовы хотитесопо- ставить каждомуимениадрес. Илиснабдить каждыйавтомобильввашейколлекцииgarageподробными техническими данными. Для этого вам потребуется другаяразновидность коллекций .NET: словарь (dictionary). Эта структура позволяет взять конкретное значение — ключ — и связать его с данными — значением. Очень важныймомент: каждый ключ встречается в словаре только одинраз. сло-варь, сущ. Список слов в алфавитном порядке с объ- яснением их значений. В реальных слова- рях ключом является определяемое слово. Оно используется для поиска значения, т. е . определения. Определение является значением, т. е . данными, связанными с определен- ным ключом (в данном случае определяемое сл ово) . Словарь Dictionary вC# объявляется так: Так вы получаете значение, связанное с ключом. Метод ContainsKey возвраща- ет true, если словарь содержит значение с указанным ключом. Обобщенныетипы для Dictionary. Тип TKey используется для клю- чей, а TValue — для значений. Таким образом, если в словаре хра- нятся слова и их определения, используется тип Dictionary<string, string>. А если вы хотите подсчитать количество вхождений каждо- го слова в книге, используйте тип Dictionary<string, int>. Dictionary<TKey, TValue> dict = new Dictionary<TKey, TValue>(); Первый тип в угловых скобках всегда относится к ключу, а второй тип всегда относится к данным. Директива «using» необходима для использования словаря. В словарь добавляются четыре пары «ключ/значение». В данном случае клю- чом является имя, а значением — люби- мая еда этого человека.
дальше 4 483 перечисления и коллекции ‘Добавление элементов. Для добавления элементов в словарь используется индексатор сугловыми скобками. Dictionary<string, string> myDictionary = new Dictionary<string, string>(); myDictionary["some key"] = "some value"; Также можно добавить элемент в словарь методом Add: Dictionary<string, string> myDictionary = new Dictionary<string, string>(); myDictionary.Add("some key", "some value"); ‘Поиск значения по ключу. Самымважным при работе со словарем является поиск значений по индексатору — собственно, они сохраняются в словаре именно для того, чтобы к ним можно было обра- щаться по уникальным ключам. Для словаря Dictionary<string, string> из нашего примера обращение к значению осуществляется по ключу string, возвращает он также строку. string lookupValue = myDictionary["some key"]; ‘Удаление элементов. Как и приработе с объектами List, для удаления из словаря используется метод Remove(). Достаточнопередать ему ключ, чтобы сам ключ и значение былиудалены из словаря: myDictionary.Remove("some key"); ‘ Получение списка ключей. Дляполучения списка всех ключей в словаре используется свойство Keys в комбинации сциклом foreach. Это делается примерно так: foreach (string key in myDictionary.Keys) { ... }; ‘Подсчет пар в словаре. Свойство Count возвращает число пар «ключ/значение», присутствующих в словаре: int howMany = myDictionary.Count; Ключи и значения могут иметь разные типы Словари чрезвычайно гибки!В нихможно хранить практическивсечтоугодно — нетолькотипы зна- чений, но и любые виды объектов. В следующем примере словарь использует ключи типаint, а ссылки на объектыDuck являютсязначениями: Dictionary<int, Duck> duckIds = new Dictionary<int, Duck>(); duckIds.Add(376, new Duck() { Kind = KindOfDuck.Mallard, Size = 15 }); Краткая сводка функциональности Dictionary Словариво многомнапоминают списки. Онигибки,позволяют вам работать сразнымитипами данных иимеют множествовстроенныхфункций.Рассмотримосновные операции, которые могут выполняться со словарями. Keys — свойство объекта Dictionary. В этом конкретном словаре используются строковые ключи, поэтому Keys представля- ет собой коллекцию строк. На практике часто встречаются сло- вари, которые связывают целые числа с объектами. Число становится уни- кальным идентификатором объекта. Ключи словаря уникальны; каждый ключ встречает- ся не более одного раза. Значения могут встречать- ся произвольное количество раз — двум ключам мо- жет соответствовать одно значение. Именно поэтому при поиске или удалении по ключу словарь знает, с какими данными должна выполняться операция.
484 глава 8 словари предназначены для поиска Построение программы с использованием словаря Вотнебольшаяпрограмма, котораяпонравитсябейсбольнымфанатам команды«Нью-ЙоркЯнкиз». Как тольковажныйигрокперестает выступать за команду,футболкас его номером перестаетиспользоваться. Создайтеновое консольноеприложение,котороепоказывает,какиеномерабылиу знаменитыхигро- ков «Янкиз» и когда эти игрокиушли избольшого спорта. Для хранения информации о бейсболистах, завершивших карьеру, будет использоваться следующий класс: class RetiredPlayer { public string Name { get; private set; } public int YearRetired { get; private set; } public RetiredPlayer(string player, int yearRetired) { Name = player; YearRetired = yearRetired; } } Нижеприведенкласс Program сметодомMain,который добавляетв словарь игроков, завершившихкарьеру. Номерфутболки может использоваться в качестве ключа словаря, потомучто он уникален, — после того, каквладелец этого номерауходитизспорта,команданикогдане использует этотномер.Этоважныйфактор, который долженучитыватьсяприпроектированииприложений, использующих словарь:выникогда не долж- ныоказатьсяв ситуации, когда вдругвыясняется, чтоключи не настолько уникальны, каквырассчитывали! using System.Collections.Generic; class Program { static void Main(string[] args) { Dictionary<int, RetiredPlayer> retiredYankees = new Dictionary<int, RetiredPlayer>() { {3, new RetiredPlayer("Babe Ruth", 1948)}, {4, new RetiredPlayer("Lou Gehrig", 1939)}, {5, new RetiredPlayer("Joe DiMaggio", 1952)}, {7, new RetiredPlayer("Mickey Mantle", 1969)}, {8, new RetiredPlayer("Yogi Berra", 1972)}, {10, new RetiredPlayer("Phil Rizzuto", 1985)}, {23, new RetiredPlayer("Don Mattingly", 1997)}, {42, new RetiredPlayer("Jackie Robinson", 1993)}, {44, new RetiredPlayer("Reggie Jackson", 1993)}, }; foreach (int jerseyNumber in retiredYankees.Keys) { RetiredPlayer player = retiredYankees[jerseyNumber]; Console.WriteLine($"{player.Name} #{jerseyNumber} retired in {player. YearRetired}"); } } } Используйте ини ци али зат ор коллекции для заполнения сло- варя объектами JerseyNumber. Йоги Берра играл под No 8 за «Нью-Йорк Янкиз», а Карл Рип- кин-младший играл под тем же номером за «Балтимор Ориолз». В словаре могут встречаться одинаковые значения, но ключи должны быть уникальными. Попытайтесь придумать способ хранения информации об игро- ках нескольких команд. Используйте цикл foreach для перебора по ключам и вывода строки для каждого игро- ка в коллекции. Сделайте это!
дальше 4 485 перечисления и коллекции ДРУГИЕ разновидности коллекций... ListиDictionary— две самые популярныеразновидностиколлекций, входящие в.NET.Они чрезвычайно гибки— доступ к их данным осуществляется в про- извольном порядке. Но иногда коллекция используется для представления сущностейреальногомира, ккоторым необходимообращаться вконкретном порядке.Дляограничениятого, какваш код обращаетсякданным коллекции, обычно используется очередь (Queue) или стек(Stack).Как иList<T>, они относятся к обобщенным коллекциям, а их главным преимуществом является обработка данных вопределенном порядке. Обобщенные коллекции .NET реализуют IEnumerable Практическивкаждомкрупномпроекте, над которымвыбудете работать, используетсятаили инаяразновидностьобобщенной коллекции, потомучтопрограммыдолжны где-то хранитьдан- ные. Группыодинаковыхобъектоввреальном мирепрактиче- скивсегда можно объединить вкатегории,которыевтойили инойстепенисоответствуют какой-то изколлекций.Неважно, какой из типов коллекций вы используете — List, Dictionary, StackилиQueue, вывсегда сможетеиспользовать с нимицикл foreach,потомучто всеониреализуютIEnumerable<T>. Используйте очередь (Queue), чтобы первый сохраненный объект обрабаты- вался первым, как в случае: ‘ автомобилей, движущихся по улице с односторонним движением; ‘ людей,стоящих вочереди; ‘ клиентов,ожидающихобслуживания по телефону; ‘ другихситуаций, когда первой обслу- живаетсяперваяпоступившаязаявка. Используйте стек (Stack), когда первым всегда должен обрабатываться последний из сохраненных объектов, как в случае: ‘ мебели, загруженной в кузов грузовика; ‘ стопкикниг, изкоторыхвыхотите всег- да сначала читать верхнюю; ‘ людей,выходящих из самолета; ‘ пирамиды из акробатов. Первым спры- гивать вниз должен тот,кто находится на самомверху.Только представьте, что произойдет, если кто-то уйдет из пира- миды с низ у! Стек работает по принципу «послед- ним вошел, первым вышел». То есть первый объект, помещенный в стек, последним покинет его. Очередь работает по прин- ципу «первым вошел, пер- вым вышел». То есть объект, первым помещенный в очередь, будет первым извлечен из очереди для обработки. Очередь похожа на список, в котором объекты добав- ляются в конец, а чтение осуществляется от начала. Стек же предоставляет доступ только к тому объ- екту, который был в него добавлен последним. Цикл foreach позволя- ет осуществлять перебор в очереди и стеке, так как они реализуют интерфейс IEnumerable! Также существуют другие разновидности коллекций, но чаще всего вам придется иметь дело именно с этими.
486 глава 8 Peek () re turne d: first in line The first Dequeue() returned: first in line T he se cond Dequeu e() r eturn ed: second in line Coun t bef ore C lear( ): 2 Count after Clear(): 0 // Создание оч ереди с добавле нием четырех эл ементов Queue<st ring> myQueue = new Queue<strin g>(); m yQueue.Enqueue( "first in line" ); m yQueue.Enqueue( "second in line "); m yQueue.Enqueue( "third in line" ); m yQueue.Enqueue( "last in line") ; // Метод Peek "подсматривает" первый элемент очереди и возвращает его без удаления Console.Writ eLine($"Peek() returned:\n{myQu eue.Peek()}"); // Метод Deque ue извлекает сл едующий элемент от НАЧАЛА очер еди Console.Writ eLine( $"The first Dequeue() retur ned:\n{myQueue. Dequeue()}"); Console.Writ eLine( $"The second Dequeue() retu rned:\n{myQueue .Dequeue()}"); // Clear удаля ет все элементы из очереди Console.Writ eLine($"Count b efore Clear():\n {myQueue.Count} "); m yQueue.Clear(); Console.Writ eLine($"Count a fter Clear():\n{ myQueue.Count}"); Очередь работает по принципу FIFO — «первым вошел, первым вышел» Очередь вцелом похожа на списоки отличается отсписка преждевсего тем, что вы неможетедобавлять иудалять элементыс произвольным индексом.Объект,по- мещаемый в очередь (Enqueue), добавляется в конец. Приизвлеченииобъекта из очереди (Dequeue) используется первый объект в начале очереди. В этом случае объектудаляется из очереди, а остальныеобъектысдвигаются на одну позицию. 1 2 3 5 4 1 2 3 5 4 Здесь метод Enqueue вызывается для добавления четырех элементов в очередь. При извлечении элементы будут возвращаться в том порядке, в каком они были добавлены. Объектам в очереди при- ходится ожи- дать своей очереди. Первый элемент, до- бавленный в оче- редь, станет первым элемен- том, который из нее выйдет. Результат После первого вызова Dequeue первый эле- мент очереди удаля- ется и возвращается методом, а второй элемент сдвигается на его место. кому нравится ожидать в очереди?
дальше 4 487 перечисления и коллекции Pee k() r eturne d: l ast in line The f irst P op() retur ned: l ast in line The s econd Pop() retu rned: third in line Cou nt be fore C lear( ): 2 Count after Clear(): 0 Стек работает по принципу LIFO — «последним вошел, первым вышел» Стекоченьпохожнаочередь — с одним принципиальным отличием. Пользователь заносит(Push)эле- менты встек, а когда их потребуется получить, данныеизвлекаются (Pop)изстека. Приизвлечении элементаиз стека выполучаете самыйпоследнийэлемент, которыйбылвнегозанесен.Стекнапоминает стопкутарелок, журналовит.д., — вы можете положить следующую тарелкунавершину, но ее необходимо снять,чтобы получить доступ к нижним тарелкам. // Создание стека с добавлением чет ырех строк S tack<string> myS tack = new Stac k<string>(); myStac k.Push("first i n line"); myStac k.Push("second in line"); myStac k.Push("third i n line"); myStac k.Push("last in line"); // Peek со стеком р аботает так же, как с очередью Console.WriteLi ne($"Peek() retu rned:\n{myStack. Peek()}"); // Pop извлекает сл едующий элемент от КОНЦА стека Console.WriteLi ne( $"The first Pop() returned:\n{my Stack.Pop()}"); Console.WriteLi ne( $"The second Pop( ) returned:\n{m yStack.Pop()}"); Console.WriteLi ne($"Count befor e Clear():\n{myS tack.Count}"); myStac k.Clear(); Console.WriteLi ne($"Count after Clear():\n{mySt ack.Count}"); 4 5 3 2 1 4 5 3 2 1 Стек создается так же, как и любая дру- гая коллекция. При занесении в стек новый элемент сдвига- ет другие элементы на одну позицию и оста- ется на вершине. Извлекая эле- мент из стека, вы получае- те элемент, который был добавлен по- следним. Последний элемент, добавленный в стек, станет первым элементом, кото- рый из него выйдет. Результат
488 глава 8 Погодите, я не поняла. А что можно сделать со стеком и очередью такого, чего нельзя сделать с List? Они всего лишь позволяют сделать код на пару строк короче, но я не имею доступа к средним элементам. Ведь то же самое можно без особых трудностей сделать со списком! Стоит ли ограничивать свои возможности ради минимальных удобств? Вам не придется ни в чем себя ограничивать при работе со стеком или оче редью . Скопировать объектQueueвобъектListочень легко. Такжелегко,какскопировать объектList в объектQueue,Queue в объектStack...более того, вы можете создать объектыList, Queue иStack из любого другогообъекта, реализующего интерфейс IEnumerable<T>. Достаточно воспользоваться перегруженнымконструктором, ко- торый позволяет передавать копируемую коллекцию в параметре. Таким образом, в вашем распоряжении появляются гибкие иудобныесредства для представления данныхввиде коллекции, котораямаксимальносоответствует вашимпотребностям. (Нонезабывайте,что прикопированиисоздаетсяновыйобъект, которыйбудетза- нимать место в куче.) Stack<string> myStack = new St ack<string>(); myS tack.Push("first in line"); myS tack.Push("secon d in line"); myS tack.Push("third in line"); myS tack.Push("last in line"); Queue<st ring> myQueue = new Queue<string >(myStack); L ist<string> myLi st = new List<st ring>(myQueue); Stack<string> anot herStack = new Stack<string>( myList); Console.Write Line($@"myQueue has {myQueue.Co unt} items myL ist has {myList. Count} items anotherStack ha s {anotherStack. Count} items"); Создадим стек с четырьмя элемен- тами — в данном случае стек строк. Стек можно легко преоб- разовать в очередь, затем скопировать в список, а потом снова скопиро- вать список в стек. Все четыре эле- мента были ско- пированы в новые колл ек ции . ...для обра ще ния ко вс ем элементам очереди или стека достаточно вос польз ова т ься цикл ом foreach! myQueue has 4 items myList has 4 items anotherStack has 4 items Результат лепешки и лесорубы
дальше 4 489 перечисления и коллекции Напишите программу, которая поможет хозяину кафе накормить лесорубов лепешками. Начните с очереди объектов Lumberjack(Лесоруб).Каждый объектLumberjack содержит стек значений из пе- речисления Flapjack (Лепешка). Чтобы вам было проще начать, мы привели некоторые подробно- сти. Сможете ли вы создать консольное приложение, которое выдает соответствующий результат? Начните с класса Lumberjack и перечисления Flapjack КлассLumberjack содержит открытое свойство Name, значение ко- торого задается в конструкторе, и приватное полеStack<Flapjack> с именем flapjackStack, инициализируемое пустым стеком. Метод TakeFlapjack получает единственный аргумент Flapjack и заносит его в стек. Метод EatFlapjacks извлекает лепешку из стека и выводит на консольданные о лесорубе. Добавьте метод Main Метод Main запрашивает у пользователя имя первого лесоруба и количество лепешек. Если пользователь вво- дит допустимое число, программа вызывает метод TakeFlapjack заданное количество раз, каждый раз передавая случайный объект Flapjack, и добавляет объект Lumberjack в очередь. Программа продолжает запрашивать дан- ные, пока пользователь не введет пустую строку, после чего в цикле while извлекает из очереди каждый объект Lumberjack и вызывает его метод EatFlapjacks для вывода данных на консоль. enum Flapjack { Cr isp y, Soggy, Browned, Ba nan a, } Lu mber jack Na me privateflapjackStack TakeFlapjack EatFlapjacks First lumberjack's name: Erik Number of flapjacks: 4 Next lumberjack's name (blank to end): Hildur Number of flapjacks: 6 Next lumberjack's name (blank to end): Jan Number of flapjacks: 3 Next lumberjack's name (blank to end): Betty Number of flapjacks: 4 Next lumberjack's name (blank to end): Erik is eating flapjacks Erik ate a soggy flapjack Erik ate a browned flapjack Erik ate a browned flapjack Erik ate a soggy flapjack Hildur is eating flapjacks Hildur ate a browned flapjack Hildur ate a browned flapjack Hildur ate a crispy flapjack Hildur ate a crispy flapjack Hildur ate a soggy flapjack Hildur ate a browned flapjack Jan is eating flapjacks Jan ate a banana flapjack Jan ate a crispy flapjack Jan ate a soggy flapjack Betty is eating flapjacks Betty ate a soggy flapjack Betty ate a browned flapjack Betty ate a browned flapjack Betty ate a crispy flapjack Объект Lumberjack выводит эту строку, когда лесоруб начина- ет есть лепешки. Этот лесоруб получил че- тыре лепешки. При вызове метода EatFlapjacks он из- влек из стека четыре значе- ния из перечисления Flapjack. Метод Main выводит эти строки и получает входные данные. Он создает каждый объект Lumberjack, задает его имя, выдает ему несколько ле- пешек из случайных категорий и добавляет в очередь. Когда пользователь завер- шает ввод данных лесорубов, метод Main в цикле while извлекает из очереди каждо- го лесоруба и вызывает его метод EatFlapjacks. Остальные строки результата выводятся объектами Lumberjack. Упражнение
490 глава 8 Ниже приведен код класса Lumberjack и метода Main. Не забудьте включить строку using System.Collections.Generic; в начало класса. class Lumberjack { private Stack<Flapjack> flapjackStack = new Stack<Flapjack>(); public string Name { get; private set; } public Lumberjack(string name) { Name = name; } public void TakeFlapjack(Flapjack flapjack) { fla pja ckS ta ck. Pus h(f lap jac k); } public void EatFlapjacks() { Console.WriteLine($"{Name} is eating flapjacks"); while (flapjackStack.Count > 0) { Console.WriteLine( $"{Name} ate a {flapjackStack.Pop().ToString().ToLower()} flapjack"); } } } class Program { static void Main(string[] args) { Random random = new Random(); Queue<Lumberjack> lumberjacks = new Queue<Lumberjack>(); string name; Console.Write("First lumberjack's name: "); while ((name = Console.ReadLine()) != "") { Console.Write("Number of flapjacks: "); if (int.TryParse(Console.ReadLine(), out int number)) { Lumberjack lumberjack = new Lumberjack(name); for(inti=0;i<number;i++) { lumberjack.TakeFlapjack((Flapjack)random.Next(0, 4)); } lumberjacks.Enqueue(lumberjack); } Console.Write("Next lumberjack's name (blank to end): "); } while (lumberjacks.Count > 0) { Lumberjack next = lumberjacks.Dequeue(); next.EatFlapjacks(); } } } решен ие упр аж нени я Метод TakeFlapjack просто заносит объект Flapjack в стек. Стек со значениями из перечисления Flapjack. Он заполняется вызовами TakeFlapjack со случайными лепеш- ками и опустошается при вызове метода EatFlapjacks. Метод Main сохраняет свою ссылку Lumberjack в очереди. Создает каж- дый объект Lumberjack, вы- зывает его ме- тод TakeFlapjack со случайными лепешками, после чего помещает ссылку в очередь. Когда пользователь завершит раздачу лепешек, метод Main в цикле while извлекает каждую ссылку Lumberjack из очереди и вызывает для нее метод EatFlapjacks. Упражнение Реше ни е
дальше 4 491 перечисления и коллекции В:Что произойдет, если я попытаюсь получить из словаря объект с несуществующим ключом? О:Еслипередать словарю несуществующий ключ, будетвыдано исключение. Например, если включить в консольное приложение следующий фрагмент: Dictionary<string, string> dict = new Dictionary<string, string>(); string s = dict["This key doesn't exist"]; вы получите исключение «System.Collections.Generic. KeyNotFoundException: ‘ключ ‘This key doesn’t exist’ отсутствует всловаре».Дляудобства в исключениевходит ключ, а конкретнее, строка, которую возвращает метод ToString ключа. Это очень по- лезно,есливы пытаетесь отладить в программе проблему,которая много тысяч раз обращается к словарю. В:Можно ли избежать этого исключения — например, если я заранее не знаю, существует ли ключ в словаре? О: Да, избежать исключения KeyNotFoundException можно дву- мя способами. Во-первых, вы можете воспользоваться методом Dictionary.ContainsKey.Методупередается ключ,которыйвы хотите использовать сословарем; онвозвращаетtrueтольков том случае, если ключ существует. Во-вторых,можновоспользоватьсяметодом Dictionary.TryGetValue: if (dict.TryGetValue("Key", out string value)) { // что-то происходит } Этот код полностью эквивалентен следующему: if (dict.ContainsKey("Key")) { string value = dict["Key"]; // do something } часто Зад авае м ые вопросы ¢ Списки, массивы и другие коллекции реализуют ин- терфейс IEnumerable<T>, который поддерживает перебор необобщенных коллекций. ¢ Циклforeachработает c любыми классами, реализу- ющими IEnumerable<T>. Интерфейс включает метод, возвращающий объект Enumerator, который позволя- ет циклу по порядку перебрать содержимое. ¢ Ковариантность представляет собой механизм C# для неявного преобразования ссылки на субкласс в ссылку на его суперкласс. ¢ Термином «неявный» обозначается способностьC# ав- томатически определить, как должно выполняться пре- образование, без выполнения явного приведения типа. ¢ Ковариантность особенно полезна при передаче кол- лекции объектов методу, который работает только с классом, от которого они наследуют. Например, ко - вариантность позволяет использовать обычное при- сваивание для повышающего приведения типа List<Subclass> в IEnumerable<Superclass>. ¢ Словарь Dictionary<TKey, TValue> — коллекция для хранения пар «ключ/значение», предоставляющая средства получения значения, ассоциированного с заданным ключом. ¢ Ключи и значения словарей могут относиться к раз- ным типам. Каждый ключ должен быть уникаль- ным в словаре, но значения могут повторяться. ¢ Класс Dictionary содержит свойство Keys, которое возвращает последовательность ключей с возможно- стью перебора. ¢ Queue<T> — коллекция, работающая по принципу «первым вошел, первым вышел»; содержит методы для постановки элемента вконец очереди и извлече- ния элемента от начала очереди. ¢ Stack<T> — коллекция, работающая по принципу «последним вошел, первым вышел»; содержит мето- ды для занесения элемента на вершину стека и из- влечения элемента с вершины стека. ¢ КлассыStack<T> иQueue<T>реализуют интерфейс IEnumerable<T> и могут быть легко преобразованы в List и другие разновидности коллекций. КЛЮЧЕВЫЕ МОМЕНТЫ
492 глава 8 загрузите следующее упражнение со страницы github Упражнение: две колоды В следующем упражнении мы построим приложение для перемещения карт между двумя колодами. Кнопкилевой колоды позволяютперетасовать колоду и вернуть ее к исходному состоянию с 52картами, а кнопки правой колоды удаляютиз колоды всекарты исортируютее. Одна из самых важных идей, которые мы неоднократно подчеркивали в книге: написание кода C# являетсяпрофессиональным навыком,а лучшийспособ совершенствованияэтогонавыка— практика. Мыхотимпредоставить вам как можно большевозможностейдляпрактики! Именнопоэтому мы создалидополнительныепроектыWindowsWPFиmacOSASP.NETCoreBlazor для некоторых главв оставшейся части книги. Мы также включилиэти проекты вконец нескольких оставшихся глав. ЗагрузитеPDF-файлс описанием проекта — мы считаем,что вам стоитзаняться им перед тем,какпереходить кследующейглаве,потомучтоэто поможет вамусвоить некоторыеважные концепции,способствующие закреплению материала оставшихся главкниги. Перейдите к репозиторию GitHub данной книги и загрузите PDF-файл проекта: https://github.com/head-first-csharp/fourth-edition TWO DECKS 10 OF SPADES JACK OF CLUBS 9 OFHEARTS 4 OF HEARTS 2 OF DIAMONDS KING OFHEARTS ACE OFHEARTS 7 OF CLUBS 9 OFDIAMONDS 3 OF SPADES ACE OFSPADES 6 OF HEARTS 3 OF HEARTS 3 OF DIAMONDS 5 OF SPADES QUEEN OF CLUBS ACE OF CLUBS 9 OFSPADES 8 OF DIAMONDS SHUF F LE SO RT CLE AR RESET DECK 1 DECK 2 При запуске приложения в левом поле содержится полная колода карт. Правое поле пусто. Двойной щелчок на карте в коло- де переводит ее в другую колоду. Таким образом, двойной щелчок на 9 пик удалит ее из колоды 2 и добавит в коло- ду 1. Кнопка Shuffle тасует коло- ду 1, а кнопка Reset возвраща- ет ее к исход- ному состоянию с 52 отсор- тированными картами. Кнопка Clear уда- ляет все карты из колоды 2, а кнопка Sort сортирует карты, чтобы они следовали по порядку.
Лабораторный курс Unity No4 Head First C# Лабораторный курс Unity No 4 493 В предыдущей лабораторной работе Unity вы на- чали строить игру. Мы использовали заготовку для создания экземпляров GameObject, которые появля- лись в случайных точках трехмерного простран- ства игры и летали по кругу. В этой лабораторной работе мы продолжим с того места, на котором остановились в предыдущей главе; в ней вы смо- жете применить то, что узнали об интерфейсах C#, и многое другое. До настоящего момента наша программа была инте- ресной визуальной моделью. В этой лабораторной работе Unity мы завершим построение игры. При запуске счет игрока равен 0. Бильярдные шары по- являются в случайных точках и летают по экрану. Когда игрок щелкает на шаре, счет увеличивается на 1 и шар исчезает. Далее появляются новые шары; если на экране появятся 15 шаров одновременно, игра завершается. Чтобы игра нормально работала, необходимо дать возможность игроку запустить ее; начать игру заново после ее завершения; а также выводить счет при щелчках на шарах. Для этого мы добавим пользовательский интерфейс, кото- рый будет выводить текущий счет в углу экрана, а также кнопку для начала новой игры. Лабораторный курс Unity No 4 Пользовательские интерфейсы https://github.com/head-first-csharp/fourth-edition
494 глава 8 Лабораторный курс Unity No 4 Вывод текущего счета Модель с летающими шарами выглядит довольно интересно, пришло время преобразовать еевигру. Добавьте новое поле вклассGameControllerдля отслеживания текущего счета — вы можете добавить его под полем OneBallPrefab: public int Score = 0; Затем добавьтевклассGameControllerметодс именем ClickedOnBall.Методбудет вызыватьсякаждый раз, когда игрок щелкаетна шаре: public void ClickedOnBall() { Score++; } Unity предоставляетвозможность объектамGameObject легко реагировать на щелчки мышью идругие способы ввода. Если вы добавите в сценарий метод OnMouseDown, Unityбудет вызывать этот метод каждыйраз,когда вы щелкаете на объектеGameObject,ккоторому онбылприсоединен. Добавьтесле- дующий метод к классу OneBallBehavior: void OnMouseDown() { GameController controller = Camera.main.GetComponent<GameController>(); controller.ClickedOnBall(); Destroy(gameObject); } Первая строка OnMouseDown получает экземпляр класса GameController, а вторая строка вызывает метод ClickedOnBall, которыйувеличиваетзначение поляScore. Запустите игру. Щелкните на строкеMainCamera в иерархии и понаблюдайте за компонентом Game Controller (Script) в окне Inspector. Щелкните на вращающихся шарах — они исчезают из сцены, а теку- щий счетувеличивается. В:Почемумы используем методInstantiateвместо ключевого слова new? О: Instantiate иDestroy — специальные методы, уникальные дляUnity.Вы не встретите их вдругихпроектахC#.МетодInstantiate несколько отличается от ключевого слова C# new, потому что он создает новый экземпляр заготовки, ане класса.Unityсоздает новые экземплярыобъектов, но также выполняетряддругихопераций(напри- мер,включениеобъектавциклобновления).Когдасценарийобъекта GameObject вызываетDestroy(gameObject), он тем самым приказывает Unity уничтожить себя. МетодDestroy приказывает Unityуничтожить объектGameObject — нотолько после завершения циклаобновления. В: Я так и не понял, как работает первая строка метода OnMouseDown. Что здесь происходит? О: Разобьем команду на части. Первая часть выглядит вполне знакомо: она объявляет переменную с именем controller типа GameController — класса, определенного вами в сценарии, присо- единенном к главной камере. Во второй части требуется вызвать метод для объекта GameController, присоединенного к главной камере. Соответственно, мы используем Camera.main для полу- ченияобъекта главнойкамеры иGetComponent<GameController>() для полу ч ения э к земпл яра G ameC ontr oll er, прис оеди ненног о к нему . часто Зад авае м ые вопросы
Лабораторный курс Unity No4 Head First C# Лабораторный курс Unity No 4 495 Включение двух режимов в игру Запустите свою любимую игру.Вы немедленно оказываетесь в гуще событий? Наверное,нет, — скореевсего,сначала откроется начальное меню. Некоторые игры позволяют игроку приостановить действие, чтобы взглянуть на карту. Многиеигры позволяют переключатьсямеждуперемещениемигрока иработой с инвентарем или в случае гибели игрока выводят анимацию, которую невоз- можно прервать. Все это примерыразныхигровыхрежимов. Добавим два режима вигрусбильярдными шарами: ‘ РежимNo 1: играработает. Шарыдобавляютсяв сцену; прищелчке шар исчезает,а счетувеличивается. ‘ Режим No2:игра завершена. Шары перестают добавляться в сцену, при щелчкахничегонепроисходит,а на экране появляетсянадпись«Gameover». Наэтом снимке экрана пока- зана игра в рабочем режиме. Шары добавляются в сцену, а игрок может щелкать на них, чтобы получать очки. Когда на экране появится послед- ний шар, игра переходит в режим завершения. На экране появля- ется кнопка PlayAgain, и новые шары перестают появляться. Чтобы добавить в игрудва режима,выполнитеследующиедействия: В игру бу- дут добав- лены два режима. Основной «игровой» режим уже реализо- ван, так что остается добавить режим за- вершения иг ры. Включите в метод GameController.AddABall поддержку текущего игрового режима НовыйусовершенствованныйметодAddABall проверяет,незавершена лиигра,исоздает новый экземпляр заготовкиOneBall только втом случае,еслиигра продолжается. ОбработчикOneBallBehaviour.OnMouseDownдолженработать только в игровомрежиме. Еслиигра закончена,она должна перестатьреагировать на щелчки. На экранедолжны остаться только ранее добавленныешары,пока игра небудет перезапущена. МетодGameController.AddABallдолжензавершитьигру,еслинаэкранеокажетсяслишком многошаров. AddABall такжеувеличивает счетчикNumberOfBalls, который повышается на 1 при каждом до- бавлениишара. Если значение достигает MaximumBalls, переменнойGameOver присваивается trueдлязавершенияигры. 1 2 3 В этой лабораторной работе мы строим игру по частям и вносим пошаговые изменения. Код каждой части можно загрузить из репозитория GitHub книги: https://github.com/head-first-csharp/fourth-edition.
496 глава 8 Лабораторный курс Unity No 4 Добавление игрового режима Внесите измененияв классыGameController и OneBallBehaviour, чтобы добавить новыережимы в игру.Дляотслеживаниязавершения игрыбудет добавлено логическоеполе. Включите в метод GameController.AddABall поддержку игрового режима. ОбъектGameControllerдолжензнать, вкаком режименаходится игра. Для отслеживания инфор- мации, доступной объекту, используются поля. Так как игра можетнаходиться в двухрежимах — игровомизавершенном, дляхранениярежима хватит логическогополя. Добавьте полеGameOver в класс GameController: public bool GameOver = false; Новыешары должны добавляться в сцену только в том случае,если игра работает. Измените ме- тодAddABall и добавьте команду if, чтобы методInstantiate вызывался только в том случае, если GameOverнесодержитtrue: public void AddABall() { if (!Ga meO ver) { Instantiate(OneBallPrefab); } } Протестируйтевнесенныеизменения.Запустите игруищелкните на объектеMainCamera вокне H i era rchy. Чтобы задать значение поля GameOver, снимите флажок вкомпонентеScript. Игра перестаетдо- бавлятьшары,пока флажокнебудет снова установлен. ОбработчикOneBallBehaviour.OnMouseDownдолженработатьтольков игровомрежиме. МетодOnMouseDownуже вызываетметодClickedOnBallклассаGameController.Теперьизменитеметод OnMouseDown вOneBallBehaviour,чтобыон такжеиспользовалполеGameOver классаGameController: void OnMouseDown() { GameController controller = Camera.main.GetComponent<GameController>(); if (!controller.GameOver) { controller.ClickedOnBall(); Destroy(gameObject); } } Сновазапустите игру иубедитесь в том, что шары исчезают, а счет увеличивается только в том случае,если игра не закончена. 1 2 Установите флажок Game Over во время выполнения игры, чтобы переключить состояние поля объ- екта GameController. Если уста- новить его во время игры, Unity сбросит значение при ее остановке.
Лабораторный курс Unity No4 Head First C# Лабораторный курс Unity No 4 497 МетодGameController.AddABallдолжензавершить игру,еслинаэкранеокажется слишком многошаров. Игра должна каким-то образомотслеживать количествошаров в сцене. Для этого мы добавим два поля в класс GameController для хранения текущего и максимального количества шаров: public int NumberOfBalls = 0; public int MaximumBalls = 15; Каждый раз, когда игрок щелкает на шаре, сценарий OneBallBehaviour шара вызывает GameController.ClickedOnBall для инкрементирования (увеличения на 1) счета. Также должно декрементироваться(уменьшаться на 1) значение NumberOfBalls: public void ClickedOnBall() { Score++; Num berO fBa lls --; } Изменитеметод AddBall,чтобы шарыдобавлялись только во времяигры,а прислишкомболь- шом количествешаровв сцене игра заканчивалась: public void AddABall() { if (!GameOver) { Instantiate(OneBallPrefab); Num ber OfB alls ++; if (NumberOfBalls >= MaximumBalls) { GameOver = true; } } } Снова протестируйте игру: запустите ее и щелкните на объекте Main Camera в окне Hierarchy. Игра должна работать как обычно, но как только поле NumberOfBalls достигнет значения MaximumBalls, методAddABall присваиваетполю GameOverзначение true иигра завершается. Когдаэтопроизойдет,щелчки на шарахникчемуне приводят, потомучтометод OneBallBehaviour. OnMouseDown проверяет полеGameOver и увеличивает счет/уничтожаетшар только в том случае,если GameOverсодержитfalse. 3 Пол е GameOver сод ержи т t ru e, если игра закончена, и false во время игры. В поле NumberOfBalls хранится теку- щееколичество шаров в сцене. Когда оно достигает значения MaximumBalls, GameC on t ro ll er пр исв аив ает GameOver значение true. Ваша игра должна отслеживать текущий игровой режим. Поля хорошо подходят для этой цели.
498 глава 8 Лабораторный курс Unity No 4 Добавление пользовательского интерфейса к игре Почтивкаждойигре, которуютолькоможнопредставить, — отPac-ManдоSuperMarioBrothers,от GrandTheft Auto5доMinecraft—присутствует пользовательскийинтерфейс(UI).Некоторые игры—скажем,Pac-Man — ограничиваютсяочень простымпользовательским интерфейсом,который выводитна экран текущий счет, рекорд, количествооставшихсяжизней и номеруровня.Вомногихиграхреализованхитроумный пользова- тельскийинтерфейс,встроенный вигровую механику(например, колесооружия, котороепозволяетигроку быстропереключатьсямеждуразнымивидамиоружия).Добавим пользовательский интерфейсв нашуигру. Выберите команду UI>>Text из меню GameObject, чтобы добавить в пользовательский интерфейс игры объектGameObject 2DText. В окнеHierarchy появляется объектCanvas,а подним — объектText: Когда вы добавляете объект Text в сцену, Unity автоматически добавляет объекты GameObject Canvasи Text. Щелкните на кнопке с треугольником ( ) рядом с объектом Canvas, чтобы раскрыть или свернуть его, — объект GameObject Text появ- ляется и исчезает, потому что он вложен в Canvas. Сделайте двойнойщелчокна объектеCanvas вокне Hierarchy, чтобывыделить его.Объект представляет собой2D-прямоугольник. Щелкните на его манипулятореMove и перетащите в сцене. Не двигается! ДобавленныйобъектCanvas всегда отображается,масштабируетсяпоразмерамэкрана ирасполагается перед всеми остальными объектамивигре. Затем сделайте двойной щелчок на объекте Text, чтобы выделить его. Онувеличивается вредакторе, но текст по умолчанию («New Text») записан вобратномнаправлении,потомучто главная камера направлена наCanvas сзади. Использование 2D-представления для работы с Canvas Кнопка2Dв верхней части окна Scene включает и отключает пред- ставление 2D: Включите 2D-представление — редактор отображает 2D-элементы на сцене. Сделайте двойной щелчок на объекте Text в окне Hierarchy, чтобы приблизить камеруктексту. Используйте колесо мыши, чтобы увеличивать и уменьшать масштаб в 2D-представлении. Кнопка 2Dиспользуется для переключения между 2D- и 3D-представлениями. Снова щелкните на ней,чтобы вернутьсяк3D-представлению. Canvas (холст) представля- ет собой двумерный объект GameObject для размещения частей пользовательского ин- терфейса игры. В нашей игре Canvas содержитдва вложен- ных объекта: только что до- бавленный объектGameObject Text, который выводит счет в правом верхнем углу, и объ- ект GameObject Buttonдля запуска новой игры. Вы обратили внимание на объ- ект EventSystem в окне Hierarchy? Unity автоматическидобавляет его при создании пользователь- ского интерфейса. EventSystem управляет мышью, клавиатурой и другими источниками ввода и от- правляет информацию объектам GameObject — все это происходит автоматически, так что вам не при- детсянапрямую работать с ним.
Лабораторный курс Unity No4 Head First C# Лабораторный курс Unity No 4 499 Настройка объекта Text для вывода счета в UI Пользовательский интерфейс игры состоит из объектовGameObject Text и Button. Каждый из этих объектовGameObject привязывается к отдельной части пользовательского интер- фейса. Например,объектGameObject Text для вывода счета будет отображатьсявправом верхнемуглу экрана(независимо от того, насколько большим илималенькимбудетэкран). ЩелкнитенаобъектеText в окнеHierarchy, чтобы выделить его, а затем просмотрите дан- ныекомпонента Rect Transform. ОбъектText должен располагаться вправом верхнемуглу, поэтому щелкните на поле Anchors на панелиRectTransform. Так как Text «живет» только внутри объекта2D Canvas, он использует компонент Rect Transform (его позиция задается относительно прямоугольни- ка Canvas). Щелкните на поле Anchors, чтобы вывести пред- варительные значения параме- тров привязки. Окно Anchor Presets позволяет привязать объекты GameObject, составляющие пользовательский интерфейс, кразличным частям Canvas. УдерживайтенажатымиклавишиAlt иShift(Option+Shift на Mac)ивыберитеправую верхнюю заготовкупривязки. Щелкни- тена тойжекнопке,которуювыиспользовали для открытия окна Anchor Presets. Теперь объектText оказывается в правом верхнем углуCanvas —сделайте на немдвойной щелчок, чтобыувеличить его. Добавимнемногосвободногоместа вышеи справа отText.Вернитесь к панелиRect Transform иприсвойтеPosX иPosY значение –10, чтобы текстрасполагался на10 единиц левее ина 10единиц ниже правого верхнегоугла. Выберите в полеAlignment компонента Textвыравниваниепо правому краюи припомощи полявверхней части окна Inspector замените имя объектаGameObject на Score. Новый объект Text должен отображаться в окне Hierarchy сименемScore. Текст выравниваетсяпо правому краю, а между краемText икраемCanvas существуетнебольшой отступ. Объект Text при- вязывается к конкрет- ной точке 2D Canvas. Удерживай- те нажаты- ми клавиши Shift и Alt (Option на Mac), чтобы задать как ось враще- ния, так и позицию. Ось вращения устанавли- вается в правом верхнем углу. Позиция Text опреде- ляется позицией якоря при- вязки относительно Canvas.
500 глава 8 Лабораторный курс Unity No 4 Кнопка для вызова метода, запускающего игру Когдаигранаходится врежиме завершения,наэкране появляетсякнопкаPlayAgain;онавызываетметод для перезапускаигры. Добавьтепустой методStartGame вклассGameController (код будетдобавленпозднее): public void StartGame() { // Код этого метода будет добавлен позднее } ЩелкнитенаобъектеCanvas в окнеHierarchy,чтобы выделить его. Выберите команду UI>>Button в меню объекта GameObject, чтобы добавить кнопку Button. Так как объектCanvas уже выделен, ре- дакторUnityдобавит новыйобъектButtonспривязкойего кцентруCanvas. Авы заметили,что рядом с объектомButton вокне Hierarchyрасполагаетсякнопкастреугольником?Она открываетвложенный объектGameObject Text. Щелкните на нем и введите текст PlayAgain. Итак, кнопка Button настроена, и теперь нужно заставить ее вызывать метод StartGame объекта GameController,присоединенногокглавной камереMainCamera.КнопкаUIпредставляет собойобъект GameObject с компонентом Button, ивы можетевоспользоваться полемOnClick()вокнеInspector длясвязыванияее сметодом-обработчиком события.Щелкнитена кнопке внижнейчастиполяOn Click(),чтобыдобавить обработчиксобытия,затемперетащитеMainCameraна полеNone(Object). Щелкните на этой кнопке, чтобы добавить обработчик собы- тия к кнопке Play Again, затем перетащите на нее объект Main Camera. Теперь кнопка знает, какой объект GameObject следует использовать для обработчика события. Щелк- нитена раскрывающемсясписке и выберите команду GameController>>StartGame. Теперь при нажатии кнопки игроком будет вызываться метод StartGame для объекта GameController, связанного с MainCamera.
Лабораторный курс Unity No4 Head First C# Лабораторный курс Unity No 4 501 Кнопка Play Again и текущий счет Пользовательский интерфейс вашей игры работает по следующей схеме: ‘ Призапускеигра переходитврежим завершения. ‘ Кнопка PlayAgain запускает игру. ‘ ОбъектText вправом верхнемуглу экрана используется для вывода текущего счета. Вкоде будут использоватьсяклассыTextи Button.Обакласса принадлежатпространству именUnityEngine. UI,поэтому незабудьтедобавить директиву usingв начало классаGameController: using UnityEngine.UI; Теперь можно добавить поля Text и Button в класс GameController (непосредственно над полем OneBallPrefab): public Text ScoreText; public Button PlayAgainButton; ЩелкнитенаобъектеMainCameraвокнеHierarchy.Перетащите объектGameObjectTextизокнаHierarchy на поле Score Text компонента Script, после чего перетащите объектGameObject Buttonна поле Button. Вернитесь ккодуGameControllerизадайтезначениепоумолчаниюдля поля GameOverравным true: public bool GameOver = true; Теперь вернитесьвUnityи проверьте компонентScript в окне Inspector. Стоп, что-то нетак! ВредактореUnityфлажокGame Overснят — значение поля не изменилось. Установите его, чтобы игра за- пускалась врежиме завершения: Игра запуститсяврежиме завершения, а игрокможет щелкнуть на кнопкеPlayAgain,чтобы начать игру. Unity запоминает значе- ния полей ваших сцена- рие в. Когда вы захотели изменить значение поля GameController.GameOver с false на true, внести изменения в код было недо- статочно. Когда вы добавляете компо- нент Script, Unity отслеживает значения полей и не перезагружает значения по умолчанию, пока не будет выполнен сброс из контекстного меню ( ). Замените false на true. Будьте осторожны!
502 глава 8 Лабораторный курс Unity No 4 Завершение кода игры Объект GameController, присоединенный к главной камере, отслеживает текущее состояние счета в поле Score. Добавьте метод Update в класс GameController,чтобы обновить текстсчета вUI: void Update() { ScoreText.text = Score.ToString(); } Затемизменитеметод GameController.AddABall,чтобы кнопкасновастановиласьдоступнойв конце игры: if (NumberOfBalls >= MaximumBalls) { GameOver = true; P lay Agai nBu tton .ga meO bjec t.S etA ctiv e(t rue ); } Осталось сделать еще одно: наладить работу метода StartGame, чтобы он запускал игру.Метод должен решать несколькозадач: уничтожить шары,которые в настоящеевремя летают в сцене, заблокировать кнопку PlayAgain, сброситьсчетиколичествошаров ипереключиться вигровойрежим. Выуже знаете, как решить большинство из этих задач! Чтобыуничтожить шары,необходимо их найти. Щелкните на заготовкеOneBall в окнеProject и задайте для неетег: Увасестьвсенеобходимоедлятого, чтобы заполнить код метода StartGame. Он использует циклforeach для нахождения иуничтожениявсех шаров,оставшихсяот предыдущейигры, скрывает кнопку,сбрасы- ваетсчетиколичество шарови изменяетрежим игры: public void StartGame() { f ore ach (Ga meOb jec t b all in Gam eObj ect .Fi ndGa meO bjec tsW ith Tag( "Ga meC ontr oll er" )) { Dest roy (bal l); } P lay Agai nBu tton .ga meO bjec t.S etA ctiv e(f als e); Score = 0; NumberOfBalls = 0; GameOver = false; } Запустите игру.Она запускается врежиме завершения. Нажмите кнопку, чтобы начать игру.Счетуве- личивается каждыйраз,когда вы щелкаетена шаре. Как только будет создан 15-й экземпляр шара, игра завершаетсяи кнопкаPlayAgain появляется снова. Каждый объект GameObject содержит свойство с именем gameObject для мани- пуляций с этим объектом. При помощи его метода SetActive можно делать кноп- ку Play Again видимой или невидимой. Тег — ключевое слово, которое можно при- соединить к любому объекту GameObject; тег может использоваться в коде для идентификации или поиска объектов. Когда вы щелкаете на заготовке в окне Project и используетераскрывающийся список для назначения тега, этот тег будет назначен каждому созданному экземпляру заготовки.
Лабораторный курс Unity No4 Head First C# Лабораторный курс Unity No 4 503 Пора потренироваться в программировании для Unity! Каждый объект GameObject содержит метод transform.Translate, перемещающий его на заданное расстояние от текущей позиции. Цель этого упражне- ния — изменить игру так, чтобы вместо transform.RotateAround для вращения шаров по оси Y сценарий OneBallBehaviour использовалtransform.Translate, чтобы шары случайным образом перемещались по сцене. • Удалитеполя XRotation, YRotation и ZRotation из OneBallBehaviour. Замените их полями для хранения скорости по осям X,Y иZ с именамиXSpeed,YSpeed и ZSpeed.Поля относятся к типуfloat, задавать их значения не обязательно. • Замените веськод метода Update следующей строкой с вызовом методаtransform.Translate: transform.Translate(Time.deltaTime * XSpeed, Time.deltaTime * YSpeed, Time.deltaTime * ZSpeed); Параметры представляют скорость перемещения шара по осямX,Y и Z.Таким образом, если значение XSpeed рав- но 1.75, после умножения на Time.deltaTime шар будетперемещаться по оси X со скоростью 1.75 единицы в секунду. • Заменитеполе DegreesPerSecond полем Multiplier со значением 0.75F — суффикс F важен! Используйте егодля обновления поляXSpeed в методе Update, затем добавьтедве аналогичные строки для полейYSpeed иZSpeed: XSpeed += Multiplier - Random.value * Multiplier * 2; В этом упражнении очень важно понять, как работает эта строка кода. Random.value — статический метод, который возвращает случайное число с плавающей точкой от 0 до 1.Что делает эта строка кода с полем XSpeed? • Затем добавьте метод с именем ResetBall и вызовите его из метода Start. Добавьте следующую строку в метод ResetBall: XSpeed = Multiplier - Random.value * Multiplier * 2; Что делает эта строка кода? Добавьте в ResetBall еще две аналогичные строки для обновления YSpeed и ZSpeed. Затем переместите строку кода, обновляющую transform.position, из метода Start в метод ResetBall. • Измените класс OneBallBehaviour, добавьте в него поле TooFar и присвойте ему значение 5. Измените метод Update, чтобы он проверял, не зашел ли шар слишком далеко. Для проверки дальности шара по оси X использу- ется следующая команда: Mathf.Abs(transform.position.x) > TooFar Команда проверяет абсолютное значение позицииX;иначе говоря, она определяет, что transform.position.x боль- ше 5F или меньше -5F. Следующая командаif проверяет, что шар зашел слишкомдалеко по осиX,Y или Z: if ((Mathf.Abs(transform.position.x) > TooFar) || (Mathf.Abs(transform.position.y) > TooFar) || (Mathf.Abs(transform.position.z) > TooFar)) { Измените метод OneBallBehaviour.Update, чтобы в нем использовалась команда if для вызова ResetBall, если шар зашел слишком далеко. Прежде чем продолжать, попробуйте определить, что делают эти строки кода. Упражнение
504 глава 8 Лабораторный курс Unity No 4 Ниже приведен код класса OneBallBehaviour после обновления по инструкциям из упражнения. Ключевое место в работе игры занимает то, что скорость каждого шара по осям X, Y и Z опре- деляется его текущими значениями XSpeed, YSpeed и ZSpeed. Внося небольшие изменения в эти значения, вы заставляете шар случайно перемещаться по сцене. using System.Collections; using System.Collections.Generic; using UnityEngine; public class OneBallBehaviour : MonoBehaviour { public float XSpeed; public float YSpeed; public float ZSpeed; public float Multiplier = 0.75F; public float TooFar = 5; static int BallCount = 0; public int BallNumber; // Start вызывается перед первым обновлением кадра void Start() { BallCount++; BallNumber = BallCount; ResetBall(); } // Update вызывается один раз на кадр void Update() { transform.Translate(Time.deltaTime * XSpeed, Time.deltaTime * YSpeed, Time.deltaTime * ZSpeed); XSpeed += Multiplier - Random.value * Multiplier * 2; YSpeed += Multiplier - Random.value * Multiplier * 2; ZSpeed += Multiplier - Random.value * Multiplier * 2; if ((Mathf.Abs(transform.position.x) > TooFar) || (Mathf.Abs(transform.position.y) > TooFar) || (Mathf.Abs(transform.position.z) > TooFar)) { ResetBall(); } } Вы добавили эти поля в класс OneBallBehaviour. Не забудьте добавить суффикс F в 0.75F, в противном случае програм- ма строиться не будет. При создании экземпляра шара его ме- тод Start вызывает ResetBall для опре- деления случайной позиции и скорости. Метод Update сначала перемещает шар, за- тем обновляет скорость и, наконец, проверяет, не вышел ли шар за границы допустимой области. Если вы выполняете эти опера- ции в другом порядке, это нормально. Упражнение Решение
Лабораторный курс Unity No4 Head First C# Лабораторный курс Unity No 4 505 void Rese tBall () { XSpee d = R andom .valu e * M ultip lier; YSpee d = R andom .valu e * M ultip lier; ZSpee d = R andom .valu e * M ultip lier; trans form. posit ion = new Vecto r3(3 - Ran dom.v alue * 6, 3 - Random.value * 6, 3 - Random.value * 6); } void OnMo useDo wn() { GameC ontro ller contr oller = Ca mera. main. GetCo mpone nt<Ga meCon troll er>() ; if (! contr oller .Game Over) { c ontro ller. Click edOnB all() ; D estro y(gam eObje ct); } } } Ниже приводятся наши ответы на вопросы — а ваши ответы похожи на них? XSpeed += Multiplier - Random.value * Multiplier * 2; Что эта строка кода делает с полем XSpeed? XSpeed = Multiplier - Random.value * Multiplier * 2; Что делает эта строка кода? Random.value * Multiplier * 2 получает случайное число от 0 до 1.5 . Вычитание его из Multiplier дает случайное число в диапазоне от –0.75 до 0.75 . Прибавление этого значения к XSpeed приводит к некоторому ускорению или замедлению шара на каждый кадр. Она присваивает полю XSpeed случайное значение от –0.75 до 0.75. В результате некоторые шары двигаются по оси X вперед, другие двигаются назад, с разными скоростями. В результате увели- чения или уменьшения скорости шара по всем трем осям каждый шар двигается по неустой- чивой случайной траек- тории. А вы заметили, что в класс GameController никаких изменений вносить не пришлось? Дело в том, что мы не изменяли ничего из того, что GameController делает, — например, управление пользовательским интерфейсом или игровой режим. Если вы можете обновить код, изменяя один класс без модификации других, это может указывать на то, что система классов была хорош о с прое кт ирова на. Сброс параметров шара происходит при создании экземпляра или при выхо- де за границы допустимой области; при этом шару присваивается случайная скорость и позиция. Если вы сначала назначаете позицию, это нормально. Упражнение Решение
506 глава 8 Лабораторный курс Unity No 4 ¢ Игры Unity отображают пользовательский интер- фейс (UI)сэлементамии графикой на двумерной пло- скости, располагающейся перед 3D-сценой игры. ¢ Unity предоставляет набор2D-объектовGameObject, предназначенных специальнодля построения пользо- вательских интерфейсов. ¢ Кнопка 2D в верхней части окнаScene включает и вы- ключает 2D-представление, упрощающее размеще- ние компонентов пользовательского интерфейса. ¢ Когда вы добавляете в Unity компонент Script, он продолжает отслеживать значения полей и не переза- гружает значения по умолчанию вплоть до сброса из контекстного меню. ¢ Кнопка Button может вызвать любой метод в сцена- рии, присоединенном к объекту GameObject. ¢ В окне Inspector можно изменять значения полей в сценариях объектов GameObject. Если вы измените их во время выполнения, они вернутся к сохраненным значениям при остановке игры. ¢ Метод transform.Translate перемещает объект Game- Object на заданноерасстояние от текущей позиции. ¢ Тег — ключевое слово , которое можно присоединить к любому объекту GameObject. Теги используются в коде для идентификации или поиска объектов. ¢ Метод GameObject.FindGameObjectsWithTag возвра- щаетколлекцию объектовGameObject с заданным тегом. Проявите фантазию! А вы можете предложить другие улучшения игры? Несколько наших идей: ‘ Игра слишкомпроста?Слишкомсложна?Попробуйте изменить пара- метры, которыевыпередаете InvokeRepeatingв методеGameController. Start.Попробуйте преобразоватьихвполя. Такжепоэкспериментируй- те созначениемMaximumBalls.Небольшие измененияэтихзначений оказывают значительное влияние на игровой процесс. ‘ Мыпредоставили готовые текстурныекартыдля бильярдныхшаров. Попробуйте добавить другие шары с другим поведением. Исполь- зуйтемасштабирование, чтобы шарыразличались вразмерах, ииз- менитеихпараметры, чтобыонидвигались быстрее илимедленнее либо по другимтраекториям. ‘ Сможетеливыреализовать шар типа «падающая звезда», который очень быстро пролетает в одном направлении и даетмного очков, если игрокуспешно щелкнет на нем? А как насчет шара типа «вне- запнаясмерть», щелчокна котором немедленно завершает игру? ‘ Измените метод GameController.ClickedOnBall, чтобы он получал параметр со счетом (вместоувеличения поля Score) и прибавлял к нему переданное вами значение. Попробуйте назначить разным шарамразное количество очков. Если вы измените поля в сценарии OneBallBehaviour, не забудьте сбросить компонент Script заготовки OneBall! Впротивном случаеонбудет «помнить» старыезначения. Чем больше опыта у вас бу- дет в написании кода C#, тем проще пойдет дело. Экспери- менты с кодом игры — отлич- ная возможность получить такой оп ыт! КЛЮЧЕВЫЕ МОМЕНТЫ
LINQ и лямбда-выражения 9 Контроль над данными И если взять первые пять слов из этой статьи и последние пять из этой и потом добавить к ним заголовок в обратном порядке, ты получишь секретное сообщение от инопланетян! Этим миром правят данные... И нам нужно знать, как в нем жить. Прошли те времена, когда можнобыло программировать днями и даже неделями, не имея дела с огромными объемамиданных. В наши дни данные стали сутью любой програм- мы. LINQ — технология C# и .NET, которая позволяет не только обращаться с запро- сами к данным в коллекциях .NET на интуитивно понятном уровне, но и группировать данные и выполнять слияние данных из разных источников. Модульные тесты помогут убедиться в том, что ваш код работает так, как предполагалось. А когда вы ос- воитесь с задачей разбиения данных на блоки, с которымиудобно работать, вы можете воспользоваться лямбда-выражениями, провести рефакторинг кода C# и сделать его еще более выразительным.
508 глава 9 Джимми — фанат Капитана Великолепного... Смотри: это кружка из ограниченной серии со второй ежегодной Великолепной конвенции, с подписями Художников исходных комиксов! Знакомьтесь: Джимми — один из самых завзятых поклонников комиксов, графическихроманов и атрибутикио КапитанеВели- колепном. Он знает оКапитане все,у него естьфигуркиизвсех фильмовиунегоестьколлекциякомиксов, которуюможноназвать только...Великолепной. насто ящи й ф анат Верно, это настоящий велико- лепномобиль из сериала, кото- рый шел с сентября по ноябрь 1973 года. Как Джимми раздо- был его? Джимми ухи- трился где-то отыскать редкую фигурку Капита- на Великолепного 2005 года в исход- ной упаковке.
дальше 4 509 LINQи лямбда-выражения Джимми— настоящийэнтузиаст, ноемуне хватает собранности. Он пытается хранить информацию о жемчужинахсвоейколлекции,но самонс этой зада- чейнесправляется. ПоможемДжиммипостроить приложениедляхранения информации о комиксах? . . . но е го коллекция разбросана по всему дому Обложка леген- дарного выпуска «Смерть Объек- та» с автогра- фами авторов. LINQработает со значениями иобъектами. Мы будем использовать LINQ для обраще- ния с запросами к числовым коллекциям длязнакомства сосновнымиидеямиисинтаксисом.Воз- можно,вы думаете: «Но как это поможет вуправлении комиксами?»Это отличный вопрос, о котором стоит помнить в первойчасти этойглавы. Позднее аналогич- ныевопросыLINQбудут использоватьсядляуправления коллекциями объектовComic. РЕ Л АКС
510 глава 9 интеграция запросов в язык Использование LINQ для управления коллекциями Вэтой главевыузнаете оLINQ(Language-IntegratedQuery).LINQобъединяет исключительнополезные классы и методы с мощнойфункциональностью, встроенной непосредственно в язык C#, для работы с последовательностямиданных,такимикакколлекция комиксовДжимми. ВоспользуемсяVisualStudio, чтобы приступить к исследованию LINQ.Создайте проект консольного приложения (.NETCore) и присвойте ему имяLinqTest. Добавьте приведенный ниже код, а когда вы доберетесь до последней строки, введитеточку и просмотритеокноIntelliSense: u sing Syste m; n amesp ace L inqTe st { using Syst em.Co llect ions. Gener ic; using Syst em.Li nq; clas s Pro gram { stati c voi d Mai n(str ing[] args ) { List<int> numbers = new List<int>(); for(inti=1;i<=99;i++) nu mbers .Add( i); I Enume rable <int> firs tAndL astFi ve = number s. } } } Директива using System.Linq; дополняетваши коллекции новыми методами для применения к ним разнообразных запросов.on them. Вы ужеработали с массивами и списками в течение некоторого времени, но эти методы вам еще не встречались. Попробуйтеудалить директиву using System.Linq;в начале класса, снова откройте окно IntelliSense — новые методы исчезли! Они отображаются только при наличии директивы using. Введите «numbers» и на- жмите . (точка), чтобы вызвать окно IntelliSense. Методов стало гораздо больше, чем прежде! Воспользуемся некоторыми из новых методовдлязавершенияконсольного приложения: I Enume rable <int> firs tAndL astFi ve = n umber s.Tak e(5). Conca t(num bers. TakeL ast(5)); foreach (int i in firstAndLastFive) { Co nsole .Writ e($"{ i} ") ; } } } } Запуститеприложение. На консоль выводитсястрока текста: 123459596979899 Чтожепроизошло? LINQ (or... — LI NQ (Language-Integrated Query) – комбинация функ- циональности C# и классов . N ET для работы с последо- вательностями данных.
дальше 4 511 LINQи лямбда-выражения Присмотримся к тому, как в програм- ме используются методы LINQ Take, TakeLast и Concat. numb ers Исходная коллекция List<int>, соз - данная в цикле for. numb ers. Take(5) Метод Take получает первые элементы последовательности. numb ers. TakeLast(5) Метод TakeLast получает последние элементы последовательности. numb ers. Take(5).Conc at(numbe rs.TakeLa st(5)) Метод Concat объединяет две последо- вательности. Запрос LINQпод увеличительным стеклом 123456789101112131415 161718192021222324252627 282930313233343536373839 404142434445464748495051 525354555657585960616263 646566676869707172737475 767778798081828384858687 888990919293949596979899 96979899100 12345 Цепочки вызовов методов LINQ При включении в код директивы using System.Linq; методы LINQ добавляются в спи- ски. Они также добавляются к массивам, очередям, стекам... собственно, они до- бавляются ко всем объектам, расширяющим IEnumerable<T>. Так как почти все методы LINQ возвращают IEnumerable<T>, вы можете получить результат ме- тода LINQ и вызвать для него другой метод LINQ без использования переменной для хранения промежуточных результатов. Этот синтаксис, называемый сцеплением вызовов, позволяет писать очень компактный код. Например, для хранения результатов Take и TakeLast можно было бы воспользо- ваться переменными: IEnumerable<int> firstFive = numbers.Take(5); IEnumerable<int> lastFive = numbers.TakeLast(5); IEnumerable<int> firstAndLastFive = firstFive.Concat(lastFive); Но сцепление вызовов позволяет объединить все вызовы в одну строку кода, с вызо- вом Concat непосредственно для результата numbers.Take(5). Оба фрагмента дела- ют одно и то же. Учтите, что компактный код не всегда лучше подробного! Ино- гда разбиение цепочки вызовов на строки упрощает ее понимание. Вы сами должны решить, какой вариант будет лучше читаться в каждом конкретном проекте. 1234596979899100
512 глава 9 linq linq для последовательностей LINQ работает с любыми реализациями IEnumerable<T> Когда вы добавляете в свой код директиву using System.Linq;, коллекция List<int>внезапно «усиливается» — в ней появляетсягруппаметодов LINQ.То же самое можно сделать с любым классом, реализующим IEnumerable<T>. ЕсликлассреализуетIEnumerable<T>,любойэкземпляр этого класса является последовательностью (sequence): ≥ Список чисел от1до 99 былпоследовательностью. ≥ Когда вы вызывалиметодTake,он возвращал ссылку на последователь- ность, содержащую первыепять элементов. ≥ Когда вы вызывалиметодTakeLast,онвозвращал другую последователь- ность из пятиэлементов. ≥ А когда вы использовалиметод Concat для объединения двух последова- тельностей из пятиэлементов,метод создал новую последовательность из 10 элементов ивернулссылку на новую последовательность. Методы LINQ перебирают последовательности Выуже знаете, что циклы foreach работают с объектамиIEnumerable. Поду- майте,что делаетциклforeach: foreach (int i in firstAndLastFive) { Console.Write($"{i} "); } Когда методработает с каждымэлементампоследовательностипо порядку, это называется перебором последовательности. Методы LINQработают именно по такому принципу. Объекты, реализующие интерфейс IEnumerable, могут быть задействованы в переборе. Эта функциональность обеспечивается объектами, реализующими интерфейс IEnumerable. Если у вас имеется объ- ект, реализую- щий интерфейс IEnumerable, вы имеете дело с последователь- ностью, которая может исполь- зоваться с LINQ. Поочередное выполнение операции с эле- ментами этой последователь- ности называется перебором этой последователь- но сти. пе-ре-би-рать, глагол. Упоминать элементы последователь- ности один за одним. Цикл foreach работает с последователь- ностью1,2,3,4,5,96,97,98,99,100. Перебор начинается с первого элемента последовательности (в данном случае 1)... ...и выполняет операцию с каждым элементом после- довательности по порядку (вывод строки на консоль).
дальше 4 513 LINQи лямбда-выражения Ниже перечислены несколько методовLINQ, которыедобавляются к последовательностям при включении директивы using System.Linq;. Имена методов интуитивно понятны — сможе- те ли вы определить, что они делают? Соедините каждый вызов метода с его результатом. А если потребуется найти первые30 выпу- сков из коллекции Джимми, начиная с вы- пуска118? LINQ предоставляет удобные методы для подобных задач. Статический метод Enumerable.Range генерирует последо- вательность целых чисел. Вызов Enumerable. Range(8, 5) возвращает последовательность из 5 чисел, начинающуюся с 8: 8, 9, 10, 11, 12. 89101112 Enumerable.Range(1, 5) .Sum() Enumerable.Range(1, 6) .Average() newint[]{3,7,9,1,10,2,-3} . Min() newint[]{8,6,7,5,3,0,9} . Max() Enumerable.Range(10, 3721) . Count() Enumerable.Range(5, 100) .L as t() new List<int>(){3,8,7,6,9,6,2} .Sk ip (4) .Sum() Enumerable.Range(10, 731) .Re ve rse () .La st () 15 3.5 -3 17 9 3 721 104 10 Вы потренируетесь в исполь- зовании метода Enumerable. Range в следующем упражне- нии с карандашом и бумагой. Возьми в руку карандаш
514 глава 9 интуитивные имена методов linq Ниже перечислены некоторые методы LINQ, которые добавляются к последовательностям при включении директивы using System.Linq;. Имена методов интуитивно понятны — сможете ли вы определить, что они делают? Соедините каждый вызов метода с его результатом. Enumerable.Range(1, 5) .Sum() E nume rabl e.Ra nge( 1, 6) .Av era ge () newint[]{3,7,9,1,10,2, -3} . Min() newint[]{8,6,7,5,3,0,9} .Max () Enumerable.Range(10, 3721) .Count() Enumerable.Range(5, 100) .L as t() new List<int>(){3,8,7,6,9,6,2} .S kip(4) .Sum() Enume rable .Rang e(10, 731) .R eve rs e() .L ast () 15 3.5 -3 17 9 3 721 104 10 Range(10, 731) возвращает последователь- ность из 731 числа, начи- ная с 10. Вы- зов Reverse переставля- ет элементы в обратном порядке, так что последним элементом ин- вертированной последователь- нос ти ст ано- вится 10. Метод Sum сум- мирует все значения последова- тельности и возвраща- ет резуль- тат. По именам методов LINQв этом упражнении нетрудно догадаться, что они делают. Некоторые методы LINQ (такие, как Sum, Min, Max, Count, First и Last) возвращают одно значение. Метод Sumсуммирует все значения в последовательности. Метод Average вычисляет их среднее значение. Методы Min и Max возвращают наименьшее и наибольшее значение в последовательности. Методы First и Last возвращают первое и последнее значение. Другиеметоды LINQ, такие как Take, TakeLast, Concat, Reverse(переставляет элементы последовательности в обратном порядке) и Skip (пропускает начальные элементы последовательности), возвращают другую последовательность. Вызов Skip(4) пропускает первые 4 элемента после- довательности, возвращая {6, 9, 2}. Sum суммирует эти элементы: 6+9+2=17. Range(5, 100) возвращает {100, 101, 102, 103, 104}, а List() по- лучает последнее число этой после- довательности. Возьми в руку карандаш Решение
дальше 4 515 LINQи лямбда-выражения Синтаксис запросов LINQ Методы LINQ, упоминавшиеся до настоящего момента, вряд ли способны ответить на все вопросы о данных, которыеу вас могли возникнуть, — или на вопросы, которые возникают уДжиммипо поводу его коллекциикомиксов. И здесь в игрувступает синтаксис декларативных запросовLINQ. В немиспользуются специальные ключевые слова — where, select, groupby,joinи т.д . —дляпостроениязапросовнепосредственновкоде. Запросы LINQ строятся из секций Построим запрос, который находит в массиве числа, меньшие 37, ивыстраивает их по возрастанию. Дляэтого используются четыре секции, которыеуказывают, ккакомуобъектудолжен бытьобращен запрос,по какому критерию отбираются элементы, как отсортиро- вать результаты икак должныбыть возвращены результаты. int[] values = new int[] {0, 12, 44, 36, 92, 54, 13, 8}; IEnumerable<int> result = from v in values wherev<37 orderby -v select v; // Вывод результатов в цикле foreach foreach(int i in result) Console.Write($"{i} "); Запрос LINQ сос тои т из четырех секций: from, where, orderby и select. Результат: 36 13 12 8 0 Секцияfrom присваивает значение переменной, называ- емой интервальной, котораябудет представлять каждое значение при переборе массива. При первой итерации переменная содержит 0, затем 12, затем 44 и т. д . Секция where содержит условие, по которому запрос опре- деляет значения, включаемые в результат, — в данном случае включаются любые значения, меньшие 37. Секция orderby содержит выражение, используемое для сортировки результатов, — в данном случае выражение -v сортируетданные по убыванию (от больших к меньшим). Запрос завершается секцией select с выражением, которое определяет, что должно быть включено в результаты. Запросы LINQ работают с после- довательностями,т.е . объектами, реализующими IEnumerable<T>. За- просы LINQ начинаются с секции fr om: from (переменная) in (последова- тельность) Она сообщает запросу, к какой по- следовательности применяется за- прос, и назначает имя, которое будет использоваться для представления каждого элемента. Секция from отчасти напоминает первую строку цикла foreach: она объявляет пере- менную, которая должна исполь- зоваться для перебора последова- тельности и которой присваивается каждый элемент последовательно- ст и. Таким образом, коман да from v in values последовательно перебирает все элементы массива values; сначала v присваивается первый элемент массива, затемвторой, затемтре- тийит.д. Начать с после дова - тельности (массива) Секция #1: Об ъя влени е ин те рва льной пере ме нной Секция #2: Включаются тол ько н еко- торые значения Секция #3: Упоря доч ение элементов Секция #4: Вы бор окон- чательных результатов 0124436 9254138 01236138 36131280 int v
516 глава 9 использование linq для создания запросов к объектам Все это, конечно, хорошо. Но как это поможет мне в управлении огромной коллекцией комиксов? LINQ работает не только с числами, но и с объектами. Когда Джиммисмотрит настопкинеупорядоченныхкомиксов, он видит бумагу, краску ихаос.Когда нанихсмотриммы, разработчики, мывидим нечто другое: большой объем данных, которые нужно упорядочить. Какупорядочить данныекомиксоввC#?Также,какмы упорядочивали игральные карты,пчелили позиции меню в забегаловкеДжо: мысоздаем класс, азатемиспользуем коллекцию дляработыс этим классом.Таким об- разом, чтобыпомочьДжимми, нужнонаписать классComic, апотом код, которыйнаведет порядок вего коллекции. А LINQнамвэтомпоможет! В: Значит, размещениедирективы using в начале файла «по волшебству»добавляет методы LINQ в каждую реализацию IEnumerable? О: По сути, да. Чтобы использовать методы LINQ (а также за- просы LINQ), необходимо включить директиву using System. Linq; в начало файла. Как было показано в последней главе, для использованияIEnumerable<T> (например, в возвращаемом значении) также должна присутствовать директива using System. Collections.Generic; . (Очевидно, никакого волшебства здесь нет. LINQ использует воз- можность C# — так называемые методы расширения, о которых вы узнаете в главе11.А покадостаточно знать, что придобавлении директивы using LINQ можетиспользоваться с любой ссылкой на IE nu merab le< T>.) В: Мне все еще непонятно, что такое «сцепленные вызовы». Как ониработают и почему ими следуетпользоваться? О:Сцеплениевызовов — очень распространенный способ вызова нескольких методов подряд.Таккакмногие методыLINQ возвращают п ос лед ова тель нос ть , реализ ую щую IE numerabl e<T >, для рез уль т ата можно вызвать другой методLINQ.Впрочем, сцепление вызовов не является отличительной особенностьюLINQ.Вы можете использовать его в своих классах. В:А вы можетепривести пример класса, в котором использу- ется сцепление вызовов? О: Конечно. Вот класс с двумя методами, построенными для сцепления: cl ass AddS ubtr act { public int Value { get; set; } pub lic AddS ubtr act Add( int i) { Con sole .Wri teLi ne($ "Val ue: {Val ue}, add ing {i} "); return new AddSubtract() { Value = Value + i }; } pub lic AddS ubtr act Subt ract (int i) { Con sole .Wri teLi ne( $ "Val ue: {Val ue}, sub trac ting {i} "); return new AddSubtract() { Value = Value - i }; } } Методы класса можно вызывать так: AddSubtract a = new AddSubtract() { Value = 5 } .A dd( 5) .Subtract(3) .A dd( 9) .S ubt rac t(1 2); Console.WriteLine($"Result: {a.Value}"); Попробуйте до- бавить класс AddSubtract в новое консоль- ное приложение, а затем добавь- те этот код в метод Main. Методы Add и Subtract классов AddSubtract возвращают экземпля- ры AddSubtract. Идеально подходит для сцепления! часто Задав аем ые вопросы
дальше 4 517 LINQи лямбда-выражения LINQ работает с объектами Джиммихочетзнать,сколькостоятсамыередкиекомиксывегоколлекции,поэтому онна- нял профессиональногооценщика.Оказывается, отдельныепозицииценятся оченьдорого! Воспользуемся коллекциями для управленияэтими данными. Создайте новое консольное приложение и добавьте класс Comic. Используйте два автоматических свойства дляназванияи номера выпуска: using System.Collections.Generic; class Comic { public string Name { get; set; } public int Issue { get; set; } public override string ToString() => $"{Name} (Issue #{Issue})"; Добавьте список List с каталогом комиксов Джимми. Добавьте статическое полеCatalog в класс Comic. Оно возвращает последовательность самыхценныхкомиксов: public static readonly IEnumerable<Comic> Catalog = new List<Comic> { new Comic { Name = "Johnny America vs. the Pinko", Issue = 6 }, new Comic { Name = "Rock and Roll (limited edition)", Issue = 19 }, new Comic { Name = "Woman's Work", Issue = 36 }, new Comic { Name = "Hippie Madness (misprinted)", Issue = 57 }, new Comic { Name = "Revenge of the New Wave Freak (damaged)", Issue = 68 }, new Comic { Name = "Black Monday", Issue = 74 }, new Comic { Name = "Tribal Tattoo Madness", Issue = 83 }, new Comic { Name = "The Death of the Object", Issue = 97 }, }; Используйте словарь для управления ценами. Добавьте статическое полеComic.Prices — это словарьDictionary<int,decimal>дляполучения ценыкаждогокомикса пономерувыпуска(используйтесинтаксис инициализатора коллек- ций для словарей, который был представлен в главе 8). Обратите внимание: интерфейс IReadOnlyDictionary используется для инкапсуляции — этот интерфейс включает только методы для чтения значений (чтобы предотвратить случайное изменение цен): public static readonly IReadOnlyDictionary<int, decimal> Prices = new Dictionary<int, decimal> { { 6, 3600M }, { 19, 500M }, { 36, 650M }, { 57, 13525M }, { 68, 250M }, {74,75M}, { 83, 25.75M }, { 97, 35.25M }, }; } 1 2 3 Редкое из- дание вы- пуска No57 (с опечат- кой) стоит $13 525. Ничего себе! Мы опустили круглые скоб- ки () в иници- ализаторах коллекции и объектов после <Comic>, потому что они необяза- тельны. Для хранения цен на комиксы исполь- зовался словарь. Также можно было добавить в класс свойство с именем Price. Мы решили хранить информацию о комиксах и ценах по отдельности. Та- кой вариант был выбран из-за того, что цены постоянно изменяются, а названия и номера выпусков остаются неизменны- ми. Как вы относитесь к этому решению? Оператор => нам еще не встречался! А вы сможете понять по контексту, что он делает?Вызнаете, как работают методыToString, — получается, что => каким-то образом заставляет ToString вернуть интер- полированную строку справа от оператора. Интерфейс IReadOnlyDictionary используется слова- рем для того, что- бы предотвратить случайное изменение цен в поле Price. Сделайте э то!
518 глава 9 Использование запроса LINQ в приложении для Джимми РанеесинтаксисдекларативныхзапросовLINQиспользовался для создания запроса,состоящего изчетырехсекций:секцииfromдлясозданияинтервальнойпеременной;секцииwhereдлявключе- ниятолькочисел,меньших37;секцииorderbyдлясортировкирезультатапоубыванию; исекции selectдля определениятого, какие элементыдолжны включатьсявитоговуюпоследовательность. Добавим в метод Main запросLINQ, которыйработает точно так же, не считая того, что он использует объекты Comic вместо значенийint, чтобы на консоль выводился список комиксов с ценой >500 в обратном порядке. Начнем с двух объявлений using, чтобы иметь возможность использовать методы IEnumerable<T> и LINQ. Запрос будет возвращать IEnumerable<Comic>, а затем использовать циклforeachдляперебора объектов ивывода результатов. Измените метод Main для использования запросов LINQ. Нижеприведен весь класс Program, включая директивы using,которые необходимо до- бавить в начало класса: using System.Collections.Generic; using System.Linq; class Program { static void Main(string[] args) { IEnumerable<Comic> mostExpensive = from comic in Comic.Catalog where Comic.Prices[comic.Issue] > 500 orderby -Comic.Prices[comic.Issue] select comic; foreach (Comic comic in mostExpensive) { Console.WriteLine($"{comic} is worth {Comic.Prices[comic.Issue]:c}"); } } } 4 подробнее о структуре запросов linq Результа т: Hippie Madness (misprinted) is worth $13,525.00 Johnny America vs. the Pinko is worth $3,600.00 Woman's Work is worth $650.00 Пространство имен System.Collections.Generic необхо- димо для использования интерфейса IEnumerable<T>, а System.Line — для добавления методов LINQ ко всем объектам, реализующим этот интерфейс. Обратите внимание на интер- вальную переменную «comic». Эта переменная типа Comic объявляется в секции from и используется в сек- циях where и orderby. Секция select определяет данные, воз- вращаемые запросом. Так как вы- бирается переменная Comic, резуль- тат запроса представляет собой IEnumerable<Comic>. В предыдущих главах было показано, что «:c» форматирует число как локальную денежную сумму, по- этому в Великобритании вместо $ будет выводиться знак фунта ₤. Использование ключевого слова descending для удобочитаемости секции orderby Всекцииorderbyиспользуетсязнак «минус» для изменения знака ценна комиксыперед сортировкой,врезультатечего запрос сортируетихпоубыванию. Впрочем, минус легко пропустить, когдавычитаете код и пытаетесьразобратьсявтом,какон работает.Ксчастью, того же результата можно добиться другимспособом. Удалите знак «минус» и добавьте ключевоеслово descending вконец секции: orderby Comic.Prices[comic.Issue] descending 5 Ключевое слово descending обеспечивает сортировку в обратном порядке.
дальше 4 519 LINQи лямбда-выражения IEnumerable<string> mostExpensiveComicDescriptions = from comic in Comic.Catalog where Comic.Prices[comic.Issue] > 500 orderby Comic.Prices[comic.Issue] descending select $"{comic} is worth {Comic.Prices[comic.Issue]:c}"; Чтобыразобратьсявтом, как работают запросыLINQ, внесем парунезначительных измененийвзапрос: ≥ Знак «минус» в секции orderby слишком легко пропустить. Используйтеклю- чевоеслово descending,добавленноена шаге5. ≥ Только что написанная секция select выбирала комикс, поэтомурезультатом запросабыла последовательность ссылокна объектыComic. Заменим его ин- терполированнойстрокой, вкоторойиспользуетсяинтервальнаяпеременная comic, — теперьрезультатзапросапредставляет собой последовательность строк. НижеприведенобновленныйзапросLINQ.Каждаясекциязапроса создает последо- вательность, которая передается следующей секции, — мы разместили под каждой секцией таблицу, в которой отображается еерезультат. Секция from пере- бирает содержи- мое ComicCatalog, извлекает из него каждое значение и присваивает ин- тервальной пере- менной «comic». Результатом сек- ции from является последователь- ность ссылок на об ъе кты Comi c. { Name = "Johnny America vs. the Pinko", Issue = 6 } { Name = "Rock and Roll (limited edition)", Issue = 19 } { Name = "Woman's Work", Issue = 36 } { Name = "Hippie Madness (misprinted)", Issue = 57 } { Name = "Revenge of the New Wave Freak (damaged)", Issue = 68 } { Name = "Black Monday", Issue = 74 } { Name = "Tribal Tattoo Madness", Issue = 83 } { Name = "The Death of the Object", Issue = 97 } { Name = "Johnny America vs. the Pinko", Issue = 6 } { Name = "Woman's Work", Issue = 36 } { Name = "Hippie Madness (misprinted)", Issue = 57 } { Name = "Hippie Madness (misprinted)", Issue = 57 } { Name = "Johnny America vs. the Pinko", Issue = 6 } { Name = "Woman's Work", Issue = 36 } "Hippie Madness (misprinted) is worth $13,525.00" "Johnny America vs. the Pinko is worth $3,600.00" "Woman's Work is worth $650.00" Секция where начинает с результатов секции from, для чего «comic» присваивается каждому значению и использует- ся для проверки условия, которое получает цену из словаря ComicPrices и включает в резуль- тат только комиксы с ценой более 500. Секция orderby начи- нает с результатов секции where и сорти- рует их по убыванию цены. Секция select перебирает результаты секции orderby, используя интервальную переменную «comic» со строковой интерполяцией для возвращения последова- тельности строк. Изменение секции select приводит к тому, что запрос возвращает последо- вательность строк. Анатомия запроса
520 глава 9 определение переменной без объявления типа Ключевое слово var позволяет C# определить тип переменной за вас Вытолько что видели, что незначительное изменение в секции select привело к изменению типа возвращаемой последовательности. Когда секция содержала select comic;, использо- вался возвращаемый тип IEnumerable<Comic>. Когда секция была приведена к виду select $"{comic} is worth {Comic.Prices[comic.Issue]:c}";, возвращаемый тип изменился на IEnumerable<string>. Приработе сLINQэтопроисходит сплошь и рядом — вы постоянно на- страиваете свои запросы. Не всегда очевидно, какойименно тип онивозвращают. Иногда не- обходимость возвращатьсяи обновлять все объявленияначинаетраздражать. Ксчастью, C# предоставляет очень полезный инструмент, который помогает сохранить объ- явления переменных простыми иудобочитаемыми. Любые изменения переменных можно заменить ключевым словом var. Таким образом,любые из следующих объявлений: IEnumerable<int> numbers = Enumerable.Range(1, 10); string s = $"The count is {numbers.Count()}"; IEnumerable<Comic> comics = new List<Comic>(); IReadOnlyDictionary<int, decimal> prices = Comic.Prices; можнозаменить следующимиобъявлениями, которые делают то же: var numbers = Enumerable.Range(1, 10); var s = $"The count is {numbers.Count()}"; var comics = new List<Comic>(); var prices = Comic.Prices; Ивамнепридетсяизменять свойкод. Простозаменитетипынаvar,и всебудет работать. С ключевым словом var C# определяет тип переменной автоматически Опробуйте внесенныеизменения.Закомментируйтепервуюстрокутолькочтонаписанногозапро- са LINQ, затемзамените IEnumerable<Count> наvar: // IEnumerable<Comic> mostExpensive = var mostExpensive = from comic in Comic.Catalog where Comic.Prices[comic.Issue] > 500 orderby -Comic.Prices[comic.Issue] select comic; Наведитеуказатель мыши на имяпеременнойв цикле foreach, чтобыувидеть ее тип: IDE определила тип переменной mostExpensive — и этоттип нам еще не встречался. Помни- те, как в главе 7 упоминалось о том, что интерфейсы могутрасширять другиеинтерфейсы? ИнтерфейсIOrderedEnumerableявляетсячастьюLINQ— он используетсядляпредставления отсортированнойпоследовательности, ион расширяет интерфейсIEnumerable<T>.Попробуйте закомментироватьсекциюorderbyи навестиуказатель мышина переменную mostExpensive — вы увидите, что она преобразовалась ктипу IEnumerable<Comic>. Дело в том, что C# анализирует код,чтобы вычислитьтиплюбой переменной, объявленной с ключевым словом var. Когда вы используете var в объявлении пере- менной, IDE определяет ее тип на основании того, как переменная используется в коде. Когда вы временно блокируете секцию orderby в этом запросе, mostExpensive преобразуется к типу IEnumerable<T>. Используя ключевое слово var, вы приказываетеC# исполь- зовать переменную с неявно определяемым типом. Термин «неявный»уже встречался вам в главе 8, когда мы рассматри- вали ковариантность. Он озна- чает, что C# определяет типы самостоятельно.
дальше 4 521 LINQи лямбда-выражения ); Развлечения с магнитами На холодильнике из магнитов был выложен запрос LINQ с ключевым словом var, но кто-то хлопнул дверью и часть магнитов упала на пол! Расставьте магниты так, чтобы за- просвыдавал результат,приведенныйв конце страницы. Console.WriteLine("Get your kicks on route {0}", {36,5,91,3,41,69,8}; int[] badgers = var skunks = pigeonin badgers orderby where select pi geo n des c ending pigeon+ 5; (pigeon!= 36 && pigeon <50) var bears = var weasels= from from sparrow inbears select sparrow - 1; skunks . Take(3); weasels.Sum() Результат: Get your kicks on route 66
522 глава 9 ); Console.WriteLine("Get your kicks on route {0}", {36,5,91,3,41,69,8}; var skunks = pigeoninbadgers orderby whe re select pige on desc end ing pigeon+ 5; (pigeon !=36&& pigeon< 50) var bears = var weasels= from from sparrow inbears select sparrow - 1; skunks . Take(3); weasels.Sum() int[] badgers = Развлечения с магнитами Решение Разместите магниты так, чтобы программа выдавала результат, приведенный внижнейча- стистраницы. LINQ начинает с некото- рой разновидности после- довательности, коллекции или массива — в данном случае массива целых чисел. Эта команда LINQ извле- кает из массива все числа, меньшие 50 и не равные 36. Каждое число увели- чивается на 5, числа сор- тируются по убыванию и помещаются в новый объект, который присва- ивается ссылке skunks. Здесь первые три числа из skunks помещаются в новую последовательность с именем bears. Команда просто уменьша- ет каждое число в bears на 1 и помещает их в weasels. Числа в weasels в сумме дают 66. После этой коман- ды skunks содержит четыре числа: 46, 13, 10и8. После этой команды bears содержит три числа: 46,13и10. После этой коман- ды weasels содер- жит три числа: 45,12и9. 45+12+9=66 Результат: Get your kicks on route 66 var может использоваться практически везде Мы намеренно выбрали невразумительные имена. Обороты типа «from pigeon in badgers» смахивают на головоломку, и понять такой код достаточно сложно. Если при- своить массиву имя «numbers» и использовать «from number in numbers» для объявле- ния интервальной переменной, это упростит чтение кода.
дальше 4 523 LINQи лямбда-выражения И высерьезно говорите, что я могу заменить любой тип в любом объявлении переменной на var и мой код все равно будет работать? Не можетбыть, чтобы все было так просто. Да, вы можете использовать var в объявлениях переменных. Да,всеможетбыть такпросто. МногиеразработчикиC#почти всегда объ- являют локальные переменные с var и включают тип только тогда, когда это упрощаетпонимание кода. Если переменная объявляетсяи инициали- зируетсяводнойкоманде,вы можетеиспользовать var. Однакодляпримененияvarсуществуетряд важныхограничений.Например: • С var можно объявить только одну переменную за раз. • Объявляемаяпеременная не может использоваться вобъявлении. • Переменнаяне может объявлятьсяравной null. Есливысоздаетепеременную с именем var, вдальнейшем вы не сможете использовать это ключевоеслово: • Ключевоесловоvar определенноне может использоватьсяв объявлени- яхполей или свойств— тольков локальныхпеременныхвнутриметода. • Придерживаясь этихбазовыхправил,вы сможетеиспользовать var практически везде. Таким обра зом , когда вы ис польз ова ли эт и объявления в главе 4: int hours = 24; short RPM = 33; long radius = 3; char initial = 'S'; int balance = 345667 - 567; Или эти в главе 6: SwordDamage swordDamage = new SwordDamage(RollDice(3)); ArrowDamage arrowDamage = new ArrowDamage(RollDice(1)); Или эти в главе 8: List<Card> cards = new List<Card>(); Также можно было использовать об ъя влени я: var hours = 24; var RPM = 33; var radius = 3; var initial = 'S'; var balance = 345667 - 567; Или эти: var swordDamage = new SwordDamage(RollDice(3)); var arrowDamage = new ArrowDamage(RollDice(1)); Или эти: var cards = new List<Card>(); ...и ва ш код работал бы точно так же. Однако var не может использоваться для объявления полей или свойств: class Program { static var random = new Random(); // Произойдет ошибка компиляции. static void Main(string[] args) {
524 глава 9 определение переменной в from и использование ее в запросе В:Как работает секцияfrom? О: У нее много общего с первой строкой цикла foreach. Один из аспектов, немного усложняющих понимание запро- сов LINQ, заключается в том , что в них выполняется не одна операция. Запрос выполняется снова и снова для каждого элементав коллекции— иначеговоря, он перебирает последо- вательность. Таким образом, секция fromрешаетдве задачи: она сообщает LINQ, какая коллекциядолжна использоваться длязапроса, и выбирает имя,котороедолжно использоваться длякаждого элемента коллекции в запросе. Создание нового именидлякаждого элемента в секцииfrom очень похоже на то, как это делается в циклеforeach.Первая строкацикла foreach выглядит так: foreach (int i in values) Цикл foreach временно создает переменную с именем i, которой последовательно присваивается каждый элемент коллекции значений. Теперь взгляните на секцию from за- проса LINQ той же коллекции: from i in values Эта секция делает практически то же самое. Она создает интервальную переменную с именем i и последовательно присваивает ей каждый элемент коллекции values. Цикл foreach выполняет одинблокдлякаждого элемента коллек- ци и, а з апрос LINQ применяет один кри терий из сек ции w he r e к каждому элементу коллекции, чтобы определить, нужно ли включить его в результаты. В:Вывзяли запросLINQ, который возвращал последо- вательность ссылок наComic, и заставили его возвращать строки. Как именно это было сделано? О: Мы изменилисекцию select.Секция select включает выражение, которое применяется к каждому элементу после- довательности, и это выражение определяет типрезультата. Такимобразом, если запрос производит последовательность значений или ссылок на объекты, вы можете воспользоваться строковой интерполяцией в секции select для преобразова- ния каждого элемента последовательности в строку. Запрос в решении завершался конструкцией select comic, поэтому он возвращал последовательность ссылок наComic. В коде из ра здела «А нат омия з ап роса» она бы ла заменена конс тру кцией select $"{comic} is worth {Comic.Prices[comic. Issue]:c}"; — и это заставило запрос вернуть последователь- ность строк вместо последовательностиComic. В:Как LINQ решает, какие данные должны быть вклю- чены в результаты? О: Для этого нужна секция select. Каждый запрос LINQ возвращает последовательность, и все элементы последова- тельности относятся к одному типу.Секция select сообщает LINQ, что должна содержать эта последовательность. Когдавы обращаетесь с запросом к массиву или списку одного типа, на- пример массивуint или List<string>, вполне очевидно, что должно попасть в секцию select.А если вы выбираете данные из списка объектов Comic?Можносделать то, что сделалДжимми, и выбрать весь класс. Также можно изменить последнюю строку запроса на select comic.Name, чтобы приказать запросу вер- нуть последовательность строк. А можноиспользовать select comic.Issue, чтобы запрос вернул последовательность int. В:Я понимаю, как использовать var в коде, но как рабо- тает это ключевое слово? О:var — ключевое слово, которое приказывает компилятору автоматически определить тип переменной на стадии компи- л яции. Компи лятор C # о предел яет тип ло каль ной пере менной, которая используется LINQдля запроса. При построении р ешения ком пилят ор зам еняет v ar т ипом, с оот ветс т вую щим данным, с которыми выработаете. Такимобразом, при компиляции следующей строки: var result = from v in values к омпил ятор зам енит va r т ипом IEnumerable<int> Вы всегда можете проверить фактический тип переменной, наведя на нее указатель мыши вIDE. Секция from в запросе LINQ решает две задачи: она сообщает LINQ, какая коллекция должна использоваться для запроса, и выбирает имя, которое должно использоваться для каждого элемента коллекции в запросе. часто Задава ем ые вопросы
дальше 4 525 LINQи лямбда-выражения ¢ Если класс реализует IEnumerable<T>, любой экзем- пляр этого класса является последовательностью. ¢ При включении using System.Linq; в начало кода методы LINQ могут использоваться с любой ссылкой на последовательность. ¢ Когда метод последовательно получает все элемен- ты последовательности, это называется перебором последовательности. Механизм перебора лежит в ос- нове всех методовLINQ. ¢ Метод Take получает первые элементы последова- тельности. Метод TakeLast получает последние эле- менты последовательности. Метод Concat объединя- ет две последовательности воедино. ¢ Метод Average возвращает среднее значение для последовательности чисел. Методы Min и Max воз- вращают наименьшее и наибольшее значение в по- следовательности. ¢ МетодыFirst иLast возвращают первый или послед- ний элемент последовательности. Метод Skip про- пускает начальные элементы последовательности и возвращает остальные. ¢ Многие методы LINQ возвращают последователь- ность, что позволяет использовать сцепление вы- зовов, т . е . вызов очередного метода LINQ непо- средственно для результата без сохранения его в промежуточной переменной. ¢ Интерфейс IReadOnlyDictionary полезен для инкап- суляции. Вы можете присвоить ему любой словарь, чтобы создать ссылку, которая не позволяет обнов- лять словарь. ¢ Синтаксис декларативных запросов LINQ исполь- зует специальные ключевые слова (where, select, groupby, join и т. д .) для построения запросов не- посредственно в коде. ¢ ЗапросыLINQ начинаются ссекции from, в которой назначается переменная, представляющая все зна- чения в процессе перебора последовательности. ¢ Переменная, объявленная в секции from, называет- ся интервальной переменной.Она может использо- ваться в запросе. ¢ Секция where содержит условие, которое использу- ется запросом для определения значений, включае- мых в результат. ¢ Секция orderby содержит выражение, используе- мое для сортировки результатов. Также в ней мож- но использовать ключевое слово descending для перехода к сортировке поубыванию. ¢ Запрос завершается секцией select с выражени- ем, которое указывает, что должно быть включено в результаты. ¢ Ключевое слово var используется для объявления переменной с неявным типом; это означает, что компи- лятор C# определяет тип переменной самостоятельно. ¢ Ключевое слово var может использоваться вместо типа переменной в каждом объявлении с инициали- зацией переменных. ¢ В секцию select можно включить выражение C#. Это выражение применяется к каждому элементуре- зультата и определяет тип последовательности, воз- вращаемой запросом. В:В запросах LINQ используются ключевые слова, которые мне еще не встречались, — from, where, orderby, select... Похоже на совершенно новый язык. Почему LINQтак отличается от остального кода C#? О: Потому что LINQ используется для другой цели. Большая часть синтаксиса C# создавалась для выполнения одной неболь- шой операции или вычисления зараз. Запуск цикла, определение п еременной, в ып ол нение математич ес кой оп ерации, в ыз ов мет ода... все это отдельные операции. Пример: var under10 = from number in sequenceOfNumbers where number < 10 select number; Запрос выглядит относительно просто — здесь не так много всего, верно? Но в действительности это довольно сложный фрагмент ко да. Подумайте, что должно произойти, чтобы программа выбрала все числа из переменной sequenceOfNumbers (которая должна содер- жать ссылку на объект,реализующийIEnumerable<T>), меньшие 10. Сначаланеобходимо в цикле перебрать весь массив. Затем каждое число сравнивается с 10.Полученныерезультаты необходимо со- брать вместе, чтобы они могли использоваться в остальном коде. Вотпочему LINQвыделяется на общемфоне: потому что запросы позволяют вместить значительный объем поведения в малом — но легко читаемом! — объеме кода C#. КЛЮЧЕВЫЕ МОМЕНТЫ
526 глава 9 Гибкость LINQ Возможности LINQ отнюдь не ограничиваются извлечениемнесколькихэлементовизколлекции. Вы можете изменить элементы перед тем, как возвращать их. После того как вы сгенерируете набор последовательностейрезультатов, LINQ предоставляет набор методов для работы с ними. Краткорассмотримнекоторые возможностиLINQ, которыеужевстречались вамранее. Измените каждый элемент, возвращаемый массивом. Этоткод присоединяет строку в конец каждогоэлемента в массиве строк. Саммассивпри этом не изменяется — вместо этогосоздается новая последовательность измененных строк. ≥ var sandwiches = new[] { "ham and cheese", "salami with mayo", " turkey and swiss", "chicken cutlet" }; var sandwichesOnRye = from sandwich in sandwiches select $"{sandwich} on rye"; foreach (var sandwich in sandwichesOnRye) Console.WriteLine(sandwich); Секцию «select» можно рассматривать как меха- низм внесения одних и тех же изменений в каждый элемент последовательности — в данном случае в конец добавляется «on rye». Теперь в конец всех воз- вращаемых объектов добавляется строка «on rye». Результат: ham and cheese on rye salami with mayo on rye turkey and swiss on rye chicken cutlet on rye Stats for these 61 numbers: The first 5 numbers: 85, 30, 58, 70 , 60 The last 5 numbers: 40, 83, 75, 26, 75 The first is 85 and the last is 75 The smallest is 2, and the biggest is 99 The sum is 3444 The a verag e is 56.46 Выполните вычисления с последовательностями. МетодыLINQмогут использоваться для получения статистики очисловой последовательности. ≥ var random = new Random(); var numbers = new List<int>(); int length = random.Next(50, 150); for(inti=0;i<length;i++) numbers.Add(random.Next(100)); Console.WriteLine($@"Stats for these {numbers.Count()} numbers: The first 5 numbers: {String.Join(", ", numbers.Take(5))} The last 5 numbers: {String.Join(", ", numbers.TakeLast(5))} The first is {numbers.First()} and the last is {numbers.Last()} The smallest is {numbers.Min()}, and the biggest is {numbers.Max()} The sum is {numbers.Sum()} The average is {numbers.Average():F2}"); запросы linq используют отложенное вычисление Ключевое слово var может использоваться для объявления массива с неявным типом. Просто используйте new[] с инициализатором коллекции, а компилятор C# определит тип массива за вас. При указании смешанного набора типов необходимо указать тип массива: var mixed = new object[] { 1, "x" , new Random() }; Статический метод String.Join выполняет конкатенацию всех элементов последовательности в строку, а также задает разделитель дляразделения элементов. Результат тестового запуска. Длина последовательности и вхо- дящие в нее числа будут случай- ными при каждом выполнении.
дальше 4 527 LINQи лямбда-выражения Запросы LINQ выполняются только при обращении к результатам Привключениивкод запросовLINQиспользуетсяотложенноевычисление. Это означа- ет, что запросLINQнасамом деле не выполняет перебор дотогомомента, когда ввашем коде будет выполнена команда, использующая результаты запроса. Возможно, это звучит немногостранно, нокогда выувидите отложенное вычисление вдействии, картина нач- нет проясняться. Создайте новоеконсольное приложение идобавьте следующийкод: class PrintWhenGetting { private int instanceNumber; public int InstanceNumber { set { instanceNumber = value; } get { Console.WriteLine($"Getting #{instanceNumber}"); return instanceNumber; } } } class Program { static void Main(string[] args) { var listOfObjects = new List<PrintWhenGetting>(); for(inti=1;i<5;i++) listOfObjects.Add(new PrintWhenGetting() { InstanceNumber = i }); Console.WriteLine("Set up the query"); var result = from o in listOfObjects select o.InstanceNumber; Console.WriteLine("Run the foreach"); foreach (var number in result) Console.WriteLine($"Writing #{number}"); } } Запустите приложение. Обратите внимание: строка Console.WriteLine, которая выводит сообщение"Set up the query",выполняетсядоget-метода. Дело втом, что запрос LINQ небудет выполнен до цикла foreach. Чтобызапросбылвыполненнемедленно,можно вызвать метод LINQ,который должен перебрать весь список, — на пример, метод ToList, который преобразует его в List<T>. Добавьте следующую строку и изменитеforeach для использования нового списка List: var immediate = result.ToList(); Console.WriteLine("Run the foreach"); foreach (var number in immediate) Console.WriteLine($"Writing #{number}"); Сновазапустите приложение. На этотразget-методы будутвызваны до того, какциклforeach начнет выполняться, — и это разумно, потому что метод ToList должен обратитьсяккаждому элементу последовательности для преобразованиявList. Такие методы, как Sum, Min и Max, тоже должны обратитьсяккаждому элементу последовательности, поэтому их применение такжезаставляетLINQнемедленно выполнить запрос. Set up the query Run theforeach Gett ing #1 Writing #1 Gett ing #2 Writing #2 Gett ing #3 Writing #3 Gett ing #4 Writing #4 Set up th e query Gettin g #1 Gettin g #2 Gettin g #3 Gettin g #4 Runthe foreach Wr iting #1 Wr iting #2 Wr iting #3 Wr iting #4 Console.WriteLine в get-методе не вызывается до момента непо- средственного выполнения цикла foreach. Отложенное вычисление выглядит так: Вызов ToList или другого метода LINQ, который об- ращается к каж- дому элементу последовательно- сти, обеспечивает немедленное вы- полнение запроса. Странная ошибка компи- лятора? Убедитесь в том, что вы добавили в код две директивы using! Сделайте эт о!
528 глава 9 использование linq для группировки данных Использование запросов group для разделения последовательности на группы Иногда данныетребуетсяразбить на группы. Допустим,Джиммихочетсгруппировать свои комиксыпо десятилетиям, в которыхонибыли опубликованы.А можетбыть,онхочет разбить их по ценам (дешевые — воднойколлекции,дорогие — вдругой).Такая группировка может пригодиться во многих случаях. И тогда вам стоитвоспользоваться запросом LINQgroup. { Suit = Hearts, Value = Jack } { Suit = Hearts, Value = Three } { Suit = Clubs, Value = Three } { Suit = Spades, Value = Four } { Suit = Diamonds, Value = Queen } { Suit = Diamonds, Value = Jack } { Suit = Clubs, Value = Two } { Suit = Clubs, Value = King } { Suit = Diamonds, Value = Queen } { Suit = Diamonds, Value = Jack } { Suit = Clubs, Value = Three } { Suit = Clubs, Value = Two } { Suit = Clubs, Value = King } { Suit = Spades, Value = Four } { Suit = Hearts, Value = Jack } { Suit = Hearts, Value = Three } from card in deck group card by card. Suit into suitGroup orderby suitGroup. Key descending select suitGroup; Создайте новое консольное приложение, добавьте классы и перечисления. Создайте новое консольное приложение .NET Core с именем CardLinq. Затем перейдите на панель Solution Explorer, щелкните правой кнопкой мыши на имени проекта и выберите команду Add>>ExistingItems (или Add>>ExistingFiles наMac).Перейдите впапку, вкоторойбыл сохраненпро- ектTwoDecks изглавы8.Добавьтефайлы с перечислениямиSuit и Value, затем классы Deck, CardиCardComparerByValue. Незабудьтеизменить пространство имен в каждом добавленномфайле и привестиего в соответствие с пространством именвProgram.cs, чтобы метод Mainмог получить доступ кдобавленным классам. Обеспечьте возможность сортировки класса Card при помощи интерфейса IComparable<T>. Мыбудем использовать секцию LINQorderby для сортировки групп, поэтому класс Cardдолжен поддерживатьсортировку.Ксчастью,даннаявозможность работает точно так же, как метод List.Sort, о котором вы узнали в главе7. ИзменитеклассCard,чтобы онрасширял интерфейсIComparable: class Card : IComparable<Card> { public int CompareTo(Card other) { return new CardComparerByValue().Compare(this, other); } // Остальной код класса остается без изменений 1 2 Мы также будем исполь- зовать методы LINQ Min и Max для нахожде- ния наибольшей и наименьшей карты в каждой группе, а они тоже использу- ют интерфейс IComparable. КлассDeck был создан в проекте «Two D ecks», который можно загрузить по ссылке в конце гл авы 8.
дальше 4 529 LINQи лямбда-выражения Измените метод Deck.Shuffle для поддержки сцепления вызовов. Класс Shuffle тасует колоду. Все, что нужно сделать для поддержки сцепления вызовов, — изменить его так, чтобы он возвращал ссылку на экземпляр Deck для только что перетасованнойколоды: public Deck Shuffle() { // Остальной код класса остается без изменений return this; } Использование запроса LINQ с секцией group...by для группировки карт по мастям. Метод Mainполучает16 случайных карт. Для этого он сначала тасует колоду, а за- тем при помощи метода LINQTake получаетпервые16 карт. Затем запросLINQ с секцией group...by используется для разделения колоды на меньшие последова- тельности,по однойдлякаждоймасти среди16 карт: using System.Linq; class Program { static void Main(string[] args) { var deck = new Deck() . Shuffle() . Take(16); var grouped = from card in deck group card by card.Suit into suitGroup orderby suitGroup.Key descending select suitGroup; foreach (var group in grouped) { Console.WriteLine(@$"Group: {group.Key} Count: {group.Count()} Minimum: {group.Min()} Maximum: {group.Max()}"); } } } 3 4 Если метод Shuffle возвра- щает ссылку на тот же объект Deck, который был перетасован, вы можете вызвать его и присоединить дополнительные вызовы ме- тодов к результату. Теперь, когда метод Shuffle поддержива- ет сцепление вызовов, вызов метода LINQ Take можно присоеди- нить к вызову Shuffle. Секция group...by в запросе LINQ разде- ляет последовательность на группы: group card by card.Suit into suitGroup Ключевое слово group сообщает, какая последовательность содержит группи- руемые элементы; ключевое слово by задает критерий, используемый для определения групп; ключевое слово into объявляет новую переменную, которая используется другимисекци- ями для ссылок на группы. Результат запроса group представляет собой последовательность последователь- ностей. Каждая группа представляется последовательностью, реализующей интерфейс IGrouping: IGrouping<Suits, Card> представляет группу карт (Card), которая использует масть (Suits) в ка- честве ключа группировки. Наведите указатель мыши на перемен- ную «grouped», чтобы увидеть ее тип. И спользуй- те м етоды LINQ Count, M inи M ax для получения ин ф ор м а- ции о каждой группе, воз- вра щ ае м ой запросо м . У каждой группы имеет- ся свойство Key, которое возвращает ее ключ — в данном случае масть. Есливам потребуется изменить имя переменной, поля, свойства, пространства имен или класса, вы можете воспользоваться удобным средством рефакторинга, встроенным в Visual Studio. Просто щелкните на имениправой кнопкой мыши и выберите в меню команду «Rename...». IDE выделяет имя, отредактируйте его — IDE автоматически переименует все его вхождения в коде. Используйте команду Rename для переименования переменных, полей, свойств, классов и про- странств имен (и не только!). При переименовании одного вхождения IDE изменяет имя везде, где оно встречается в коде. Подсказки для IDE: время менять имена!
530 глава 9 слияние последовательностей в запросахjoin var grouped = from card in deck group card by card.Suit into suitGroup orderby suitGroup.Key descending select suitGroup; Давайте повнимательнееразберемся втом, какработают запросыgroup. Секция from работает точно так же, как в дру- гих использованных нами запросах LINQ. Она при- сваивает диапазонной пе- ременной «card» каждую карту в последователь- ности — в данном случае в объекте Deck, который вы перетасовали и взяли из него несколько карт. Секция group...by разбива- ет карты на группы. Она включает выражение «by cardSuit» — оно сообщает, что ключом каждой группы является масть карты. Оно также объявляет новую пе- ременную с именем suitGroup, которая будет использо- ваться остальными секциями для работы с группами. { Suit = Hearts, Value = Jack } { Suit = Hearts, Value = Three } { Suit = Clubs, Value = Three } { Suit = Spades, Value = Four } { Suit = Diamonds, Value = Queen } { Suit = Diamonds, Value = Jack } { Suit = Clubs, Value = Two } { Suit = Clubs, Value = King } Эта случайная выборка по случайности на- чинается с двух карт одной масти (червы), за которыми идет карта пиковой масти и две бубновые карты. Сек ция gro up ...by перебирает последо- вательность, созда- вая новые группы с обнаружением каж- дого нового ключа. Таким образом, груп- пы следуют в том же порядке, в каком вхождения мастей впервые встречаются в случайной выборке. { Suit = Diamonds, Value = Queen } { Suit = Diamonds, Value = Jack } { Suit = Clubs, Value = Three } { Suit = Clubs, Value = Two } { Suit = Clubs, Value = King } { Suit = Hearts, Value = Jack } { Suit = Hearts, Value = Three } { Suit = Spades, Value = Four } { Suit = Diamonds, Value = Queen } { Suit = Diamonds, Value = Jack } { Suit = Clubs, Value = Three } { Suit = Clubs, Value = Two } { Suit = Clubs, Value = King } { Suit = Hearts, Value = Jack } { Suit = Hearts, Value = Three } { Suit = Spades, Value = Four } Секция group...byсоздает последовательность групп, реализующую интерфейс IGrouping. IGrouping рас- ширяет IEnumerable и до- бавляет ровно один ком- понент: свойство с именем Key. Таким образом, каждая группа представляет собой последовательность других последовательностей — в данном случае это группа последовательностей Card, у которой ключом является масть карты (из перечис- ления Suits). Полный тип каждой группы имеет вид IGrouping<Suits, Card>, что означает, что это последова- тельность последователь- ностей Card, каждая из ко- торых использует значение Suits в качестве ключа. Секция group...by группирует карты по cardSuit, так что ключом каждой груп- пы является масть. Это означает, что все карты в каждой группе имеют оди- наковую масть и все карты этой масти принадлежат данной группе. Секция orderby сортирует группы по ключу, в результате чего они располагают- ся в порядке их вхож- дения в перечень Suits (в обратном порядке): Spades, Hearts, Clubs и Diamonds. Анатомия запроса group
дальше 4 531 LINQи лямбда-выражения L i s t< R e v i e w > on comic.Issue equals review.Issue П о с л е д ов а т е л ь н о с т ь L I N Q результаты L i s t < C o m i c > f r o m c o m i c i n C o m i c . C a t a l o g j o i n r e v i e w i n r e v i e w s Использование запросов join для слияния данных из двух последовательностей Каждый опытный коллекционер знает, что критические отзывысильно влияютна цены.Джим- ми следитза рейтингамина двух крупнейших агрегаторахотзывовна комиксы, MuddyCritic иRotten Tornadoes. Теперьон хочет сопоставить ихсосвоейколлекцией.Как емуэтосделать? На помощь приходит LINQ!Ключевое слово join позволяет объединить данные из двух после- довательностей однимзапросом.Дляэтого элементыодной последовательности сравниваются ссоответствующими элементами второйпоследовательности.(LINQхватаетсообразительности на то, чтобыделать это эффективно, — каждаяпараэлементов сравниваетсятольков томслучае, если это действительнонеобходимо.)Окончательныйрезультат объединяет парыссовпадением. Запрос начинается с обычной секции from. Но вместо того чтобы указывать критерии отбора данныхдлявключенияв результаты, выдобавляете следующую конструкцию: join имя in коллекция Секция join приказывает LINQперебрать обе по- следовательности, чтобы сопоставить пары, вклю- чающие поодномуэлементукаждойпоследователь- ности.Имя присваиваетсяэлементу,которыйбудет извлекаться из объединенной коллекции при каж- дой итерации. Этоимяиспользуетсяв секции where. Далеев запросе LINQ следу- ют секции where и orderby, как обычно. Запрос может завершаться обычной сек- цией select, но чаще всего возвращаются результаты, которые извлекают некие данные из одной коллекции, и другие данные из другой ко лл екц ии. 3 Затем добавляется секция on, которая со- общает LINQ, как следует объединять две коллекции. Заключевым словомследует имя элемента первой коллекции, ключевое слово equals и имя элемента второй коллекции, по которому они должны связываться. 2 class Review { public int Issue { get; set; } public Critics Critic { get; set; } public double Score { get; set; } } Джимми объ- единяет комиксы из своей коллек- ции с данными об обзорах по каж- дому выпуску. Данные Джимми хранят- ся в коллекции объектов Review с именем «reviews». Результатом являетсяпоследовательность объектов, которыевключают свойстваNameиIssueизComic, а такжесвойстваCriticиScoreизReview. Результат неможетбыть последовательностью объектовComic, нотакженеможет быть ипоследовательностью объектовReview,потомучтониодинкласс несодержит все этисвойства. Результатом является другаяразновидность типа: анонимный тип. { Name = "Woman's Work", Issue = 36, Critic = MuddyCritic, Score = 37.6 } { Name = "Black Monday", Issue = 74, Critic = RottenTornadoes, Score = 22.8 } { Name = "Black Monday", Issue = 74, Critic = MuddyCritic, Score = 84.2 } { Name = "The Death of the Object", Issue = 97, Critic = MuddyCritic, Score = 98.1 } 1
532 глава 9 реальные типы без имен Использование ключевого слова new для создания анонимных типов Вы использовали ключевое слово new для создания экземпля- ровклассовещес главы3.Каждыйраз, когдавы используете его,вы включаетевпрограммуновыйтип(такчто команда new Guy();создает экземпляр типа Guy).Такжеключевое слово new можетиспользоватьсябезтипа длясозданияано- нимного типа. Это абсолютно допустимый тип, который содержиттолько свойства для чтения,но не имеет имени. Нашзапросвозвращает тип,которыйобъединяет комиксы Джиммисобзорами. Этот тип является анонимным. Вы можете добавить свойства в анонимный тип при помощи инициализатора объекта. Воткакэто выглядит: public class Program { public static void Main() { var whatAmI = new { Color = "Blue", Flavor = "Tasty", Height = 37 }; Console.WriteLine(whatAmI); } } Попробуйте вставить кодв новое приложение ивыполнить его. Вы получите следующий результат: { Color = Blue, Flavor = Tasty, Height = 37 } Теперь наведите указатель мыши на whatAmI вIDE и взгляните на окно IntelliSense: IDE точно знает, что это за тип: это объектный тип с двумя строковыми свойствами и свой- ством int. Просто у этого типа нет имени, поэтому он называет- ся анонимным типом. ПеременнаяwhatAmI относится кссылочномутипу, каклюбаядругаяссылканаобъект. Онауказывает наобъект, находящийсявкуче, иможет использоватьсядляобращения к компонентам этого объекта,вданном случаек двумего свойствам: Console.WriteLine($"My color is {whatAmI.Color} and I'm {whatAmI.Flavor}"); Крометогофакта,чтоуанонимныхтиповнетимен, онинеотличаются отвсех остальных типов. а-но -ним -ный, прил. неидентифицируемый по име- ни. А н о н и м н ы й о б ъ е к т w h a t A m I Теперь я понимаю, почему мы изучали ключевое слово var. Оно нужнодля объявления анонимных типов. Приведенный ранее запрос LINQ, объединяющий комиксы Джимми с обзорами, возвра- щает анонимный тип. Этот запрос будетдобавлен в при- ложение позднее в этой главе. Верно! Ключевое слово var используется для объявления аноним ных т ипов. Собственно, этоодноизсамых важныхпримененийключевогословаvar.
дальше 4 533 LINQи лямбда-выражения Джо, Боб и Алиса — ведущие мировые игроки в Go Fish. Код LINQ объединяет два массива с анонимными типами для гене- рирования списка их выигрышей. Прочитайте код и запишите результат, который будет выведен на консоль. var players = new[] { new { Name = "Joe", YearsPlayed = 7, GlobalRank = 21 }, new { Name = "Bob", YearsPlayed = 5, GlobalRank = 13 }, new { Name = "Alice", YearsPlayed = 11, GlobalRank = 17 }, }; var playerWins = new[] { new { Name = "Joe", Round = 1, Winnings = 1.5M }, new { Name = "Alice", Round = 2, Winnings = 2M }, new { Name = "Bob", Round = 3, Winnings = .75M }, new { Name = "Alice", Round = 4, Winnings = 1.3M }, new { Name = "Alice", Round = 5, Winnings = .7M }, new { Name = "Joe", Round = 6, Winnings = 1M }, }; var playerStats = from player in players join win in playerWins on player.Name equals win.Name orderby player.Name select new { Name = player.Name, YearsPlayed = player.YearsPlayed, GlobalRank = player.GlobalRank, Round = win.Round, Winnings = win.Winnings, }; foreach (var stat in playerStats) Console.WriteLine(stat); { Name = Alice, YearsPlayed = 11, GlobalRank = 17, Round = 2, Winnings = 2 } { Name = Alice, YearsPlayed = 11, GlobalRank = 17, Round = 4, Winnings = 1.3 } Этот код выводит шесть строк на консоль. Мы заполнили первые две строки, чтобы вам было проще. Обратите внимание: обе строки со- держат одинаковые имена («Alice»). Запрос join находит все совпадения между ключевыми свойствами обеих последовательностей. Если совпа- дений несколько, результаты будут включать по одному элементу для каждого совпадения. Если во вход- ной последовательности присутству- ет ключ, для которого нет совпаде- ния в другой последовательности, он не включается в результаты. Мы используем var и new[] для создания массивов с ано- нимными типами. Возьми в руку карандаш
534 глава 9 выделение методов для улучшения кода Джо, Боб и Алиса — ведущие мировые игроки в Go Fish. Код LINQ объединяет два массива с анонимными типами для гене- рирования списка их выигрышей. Прочитайте код и запишите результат, который будет выведен на консоль. { Name = Alice, YearsPlayed = 11, GlobalRank = 17, Round = 2, Winnings = 2 } { Name = Alice, YearsPlayed = 11, GlobalRank = 17, Round = 4, Winnings = 1.3 } { Name = Alice, YearsPlayed = 11, GlobalRank = 17, Round = 5, Winnings = 0.7 } { Name = Bob, YearsPlayed = 5, GlobalRank = 13, Round = 3, Winnings = 0.75 } { Name = Joe, YearsPlayed = 7, GlobalRank = 21, Round = 1, Winnings = 1.5 } { Name = Joe, YearsPlayed = 7, GlobalRank = 21, Round = 6, Winnings = 1 } В: Можетеещераз объяснить, как работает var? О: Да, разумеется. Ключевое слово var решает неочевидную проблему, которая возникает при использовании LINQ. Обычно при вызове метода или выполнении команды абсолютно ясно, с какими типами вы работаете. Например, если у вас есть метод, который возвращает строку, можно сохранить результаты в строковой переменной или в поле. Однакос LINQ всенетакпросто. Когда вы строите команду LINQ, она может вернуть анонимный тип, который не определен нигде в программе. Да, вы знаете, что это будет некая последователь- ность. Но что это за последовательность?Неизвестно — потому что объекты, содержащиеся в последовательности, полностью зависят от содержимого запросаLINQ. Для примера возьмем следующий запрос из кода, написанного ранеедля Джимми. Изначальнобыл написан следующий запрос: IEnumerable<Comic> mostExpensive = from comic in Comic.Catalog where Comic.Prices[comic.Issue] > 500 orderby -Comic.Prices[comic.Issue] select comic; Но потом первая строка была изменена для использования клю- чевого слова var: var mostExpensive = И этоудобно. Например, если изменить последнюю строку и при- вести ее к следующему виду: select new { Name = comic.Name, IssueNumber = $"#{comic.Issue}" }; обновленный запрос вернет другой (ноабсолютно допустимый!) тип— анонимный тип сдвумя компонентами: строкойName и стро- кой IssueNumber. Но в нашей программе нет определения класса дляэтоготипа!Вам не нужно запускать программу, чтобыувидеть, какопределяется этот тип,но переменная mostExpensiveдолжна быть объявлена с каким-то типом. Вотпочему C# предоставляет ключевое слово var, которое сооб- щаеткомпилятору: «Да, мы знаем, что это допустимый тип, но пока не можем точно сказать, что это за тип. Может, ты это как-нибудь сам определишь и не будешь насбеспокоить?Большоеспасибо». часто Зад авае м ые вопросы Возьми в руку карандаш Решение
дальше 4 535 LINQи лямбда-выражения Переключение переменных между неявными и явнымитипами При работе с запросами с группировкой часто используется ключевое слово var — не только потому, что это удобно, но и потому, что тип, возвращаемый запросом, может быть довольно громоздким. var grouped = from card in deck group card by card.Suit into suitGroup orderby suitGroup.Key descending select suitGroup; Но иногда наш код проще понять при использовании явного типа. К счастью, IDE упрощает переключение междунеявным типом (var) и явным типом переменной. Откройте меню QuickActions и выберите «Use explicit type instead of ‘var’», чтобы преобразовать var в явный тип. Вытакже можетевыбрать «Use implicit type» из меню QuickActions, чтобы вернуть переменную к объявлению с var. Выделение методов Часто ваш код будет проще читаться, если вы возьмете большой метод и разобьете его на несколько меньших. Вот почему одним из самых распространенных приемов рефакторинга кода является выделение методов, т . е. извлечение блока кода из большого метода и перемещение его в отдельный метод. IDE предоставляет в ваше распоряжение полезные инструменты, упрощающие решение этой задачи. Дляначала выделите блок кода. Затем выберите команду Refactor>>Extract Method из меню Edit(в Windows) или Extract Method (на Mac) из менюQuickActions. Как только вы это сделаете, IDE переместит выделенный код в новый метод с именем NewMethod и возвращаемым типом, соответствующим типу, возвращаемому кодом. Затем IDE немедленно активизирует функцию переименования, чтобы вы могли начать ввод нового имени метода. На этом снимкевыделен весь запросLINQ из проекта группировки карт, приведенного ранее в этой главе. После выделения метода он выглядит так: При этом остается новая переменная grouped с явной типизацией — IDE определила, что эта переменная используется позднее в коде. Это еще один пример того, как IDE помогает писать более чистый код. Подсказки для IDE: средства рефакторинга Меню Quick Actions может использовать- ся для замены неявного типа «var» явным ти- пом — в данном случае IOrderedEnumerable<I Grouping<Suits, Card>> (согласитесь, вводить такое имя никому не захочется!).
536 глава 9 var не может использоваться с полями и параметрами В:Можетечуть подробнеерассказать, как работает join? О:join работает сдвумя последовательностями. Допустим, вы печатаете футболки для игроков, используя коллекцию с именем players. Коллекция содержит объекты со свойством Name и свой- ством Numbers. А если для игроков с двузначными номерами потребуется специальныйдизайнфутболок?Из коллекции можно извлечь игроков с номерами, большими 10: var doubleDigitPlayers = from player in players where player.Number > 10 select player; Аеслинеобходимо еще получить размеры футболок?Если у вас имеется последовательность с именемjerseys, элементы которой содержат свойство Number и свойство Size, запрос join хорошо подойдетдля объединенияданных: var doubleDigitShirtSizes = from player in players where player.Number > 10 join shirt in jerseys on player.Number equals shirt.Number select shirt; В: Запрос возвращает набор объектов. А если я хочу связать каждого игрока с его размером футболки, а номер меня не интересует? О: Для таких ситуаций нужны анонимные типы — вы можете сконструировать анонимный тип, который содержит только те данные, которые вам нужны. Также анонимные типы позволяют выбрать данные из разных коллекций, которые вы объединяете. Такимобразом, вы можете выбрать имя иразмерфутболки игрока, и ничего более: var doubleDigitShirtSizes = from player in players where player.Number > 10 join shirt in jerseys on player.Number equals shirt.Number select new { player.Name, shirt.Size }; IDE достаточноумна, чтобы понять, какие результаты будет соз- давать запрос. Если вы создадитециклдля переборарезультатов, то при вводе имени переменнойIDE откроет список IntelliSense: foreach (var size in doubleDigitShirtSizes) size. Обратите внимание: список содержит свойства Name и Size. Если вы добавите новые элементы в секцию select, они также по- явятся в списке, потому что запрос создаст новый анонимный тип с дру г ими компо нент ами. В: Какнаписать метод, возвращающий анонимный тип? О:Никак. Методы не могут возвращать анонимные типы. C# не дает такой возможности. Также невозможно объявить поле или свойство с анонимнымтипом илииспользовать анонимный типдля параметра метода или конструктора, именнопоэтому вы не cможете использовать ключевое слово var ни в одной из этих ситуаций. И если задуматься, все это вполне логично. Каждый раз, когда вы используете var в объявлении переменной, вы всегдадолжны привести значение, которое используется компилятором C# или IDEдляопределения типа переменной.Если вы объявляете поле или параметр метода, указать это значение невозможно, а следо- вательно, C# не сможет определить тип. (Да, для свойства можно задать значение, но это не одно и то же — формально значение задается только перед вызовом конструктора.) Ключевое слово var может использоваться только при объявлении переменных. Оно не может использоваться с полем или свойством или для написания метода, который возвращает анонимный тип или получает его в параметре. часто Зад авае м ые вопросы
дальше 4 537 LINQи лямбда-выражения Используйте то, что вы узнали от LINQ, для построения нового консольного приложения с именем JimmyLinq, которое упорядочивает коллекцию комиксов Джимми. Начните с добав- ления перечисления Critics с двумя компонентами, MuddyCritic и RottenTornadoes, и перечисления PriceRange с двумя компонентами Cheap и Expensive. Затем добавьте класс Review с тремя автоматическими свойствами: intIssue, Critics Critic и double Score. Вампонадобятсяданные, поэтомудобавьте в классComicстатическое поле, возвращающее последовательность обзоров: public static readonly IEnumerable<Review> Reviews = new[] { new Review() { Issue = 36, Critic = Critics.MuddyCritic, Score = 37.6 }, new Review() { Issue = 74, Critic = Critics.RottenTornadoes, Score = 22.8 }, new Review() { Issue = 74, Critic = Critics.MuddyCritic, Score = 84.2 }, new Review() { Issue = 83, Critic = Critics.RottenTornadoes, Score = 89.4 }, new Review() { Issue = 97, Critic = Critics.MuddyCritic, Score = 98.1 }, }; Метод Main и два метода, которые из него вызываются: static void Main(string[] args) { var done = false; while (!done) { Console.WriteLine( "\nPress G to group comics by price, R to get reviews, any other key to quit\n"); switch (Console.ReadKey(true).KeyChar.ToString().ToUpper()) { case "G": done = GroupComicsByPrice(); break; case "R": done = GetReviews(); break; default: done = true; break; } } } private static bool GroupComicsByPrice() { var groups = ComicAnalyzer.GroupComicsByPrice(Comic.Catalog, Comic.Prices); foreach (var group in groups) { Console.WriteLine($"{group.Key} comics:"); foreach (var comic in group) Console.WriteLine($"#{comic.Issue} {comic.Name}: {Comic.Prices[comic.Issue]:c}"); } return false; } private static bool GetReviews() { var reviews = ComicAnalyzer.GetReviews(Comic.Catalog, Comic.Reviews); foreach (var review in reviews) Console.WriteLine(review); return false; } Ваша задача — создать статический класс с именем ComicAnalyzer с тремя статическими методами (два из которых объявлены открытыми): • Приватный статический метод с именем CalculatePriceRange получает ссылку на Comic и возвращает PriceRange.Cheap, если цена ниже 100, или PriceRange.Expensive в противном случае. • GroupComicByPrice упорядочивает комиксы по цене, а затем группирует их по CalculatePriceRange(comic) и возвращает последовательность групп комиксов (IEnumerable<IGrouping<PriceRange, Comic>>). • GetReviews упорядочивает комиксы по номеру выпуска, а затем выполняет объединение (см. ранее в этой главе) и возвращает последовательность строк следующего вида:MuddyCritic rated #74 'Black Monday' 84.20 Присмотритесь к циклу while. Он использует команду switch для определения вызываемого метода. Метод воз- вращает true, присваивая «done» значение false, а цикл while выполняет очередную итерацию. Если пользова- тель нажимает любую другую клавишу, кроме G и R, то «done» присваивается true, а цикл завершается. Методы GroupComicsByPrice и GetReviews вызывают ме- тоды статического класса ComicAnalyzer (который вскоре будет написан), которые выполняют запросы LINQ. Циклы foreach в методе GroupComicsByPrice являются вложен- ными: одинцикл выполняется внутри другого. Внешнийцикл выводит ин- формацию о каждой группе, а внутрен- ний перебирает группу. Упражнение
538 глава 9 LINQ и лямбда-выражения Используйте то, что вы узнали от LINQ, для построения нового консольного приложения с име- нем JimmyLinq, которое упорядочивает коллекцию комиксов Джимми. Начните с добавления пере- числения Critics с двумя компонентами, MuddyCritic и RottenTornadoes, и перечисления PriceRange с двумя компонентами, Cheap и Expensive. Затем добавьте класс Review с тремя автоматическими свойствами: int Issue, Critics Critic и double Score. Добавьте перечисления Critics и PriceRange: Затем добавьте класс Review: enum Critics { Mu ddy Cri tic, Ro tte nTo rnad oes , } enum PriceRange { Cheap, Ex pen siv e, } Когда это будет сделано, добавьте статический класс ComicAnalyzer с приватным методом PriceRange и открытыми методами GroupComicByPrice иGetReviews: using System.Collections.Generic; using System.Linq; static class ComicAnalyzer { private static PriceRange CalculatePriceRange(Comic comic) { if (Comic.Prices[comic.Issue] < 100) return PriceRange.Cheap; else return PriceRange.Expensive; } public static IEnumerable<IGrouping<PriceRange, Comic>> GroupComicsByPrice( IEnumerable<Comic> comics, IReadOnlyDictionary<int, decimal> prices) { IEnumerable<IGrouping<PriceRange, Comic>> grouped = from comic in comics orderby prices[comic.Issue] group comic by CalculatePriceRange(comic) into priceGroup select priceGroup; return grouped; } public static IEnumerable<string> GetReviews( IEnumerable<Comic> comics, IEnumerable<Review> reviews) { var joined = from comic in comics orderby comic.Issue join review in reviews on comic.Issue equals review.Issue select $"{review.Critic} rated #{comic.Issue} '{comic.Name}' {review.Score:0.00}"; return join ed; } } class Review { public int Issue { get; set; } public Critics Critic { get; set; } public double Score { get; set; } } А вы столкнулись с ошибкой компилятора «непоследовательныеуровни доступа», которая сообщает, что возвращаемый тип обладает меньшей доступностью, чем метод? Это происходит тогда, когда класс помечен как открытый, но содержит компоненты с внутренней доступностью (что происходит, если модификатор доступа не указан). Проследите за тем, чтобы классы или перечисления небыли помечены как открытые. Средства рефакторин- га, представленные ранее в этой главе, упрощают правильное определение возвращаемого типа ме- тода GroupComicsByPrice. Мы предложили вам упо- рядочить комиксы по цене, а затем сгруппировать их. В результате каждая группа будет отсортиро- вана по цене, потому что группы создаются в поряд- ке перебора последователь- ности секцией group...by. Очень похоже на запрос join, описанный ранее в этой главе. Не забудьте про директи- вы using. Мы намереннодопустили ошибку в методе CalculatePriceRange. Проследите за тем, чтобы код вашего метода совпадал с кодом этого решения. А вы сможете найти ошиб- ку? Она весьма хитроумна... Упражнение Реше н ие
дальше 4 539 LINQи лямбда-выражения ¢ Секция group...by приказывает LINQ сгруппировать результаты — при использовании этой секции LINQсоз- дает последовательность последовательностей групп. ¢ Каждая группа содержит элементы с общим элемен- том, который называется ключом группы. Исполь- зуйте ключевое слово by для определения ключа для группы. Каждая последовательность группы со- держит компонент Key, являющийся ключом группы. ¢ Запросы join используют секцию on...equals, чтобы сообщить LINQ, как следует связывать пары элемен- тов. ¢ Секция join используется для объединения двух коллекций в одном запросе. При этом LINQ сравни- вает каждый элемент первой коллекции с каждым элементом второй коллекции и включает врезультат пары с совпадениями. ¢ При выполнении запроса join вы обычно хотите полу- чить набор результатов, включающий некие данные из одной коллекции и другие данные из другой кол- лекции. Секция select позволяет построить нужный результат из обеих коллекций. ¢ Используйте select new для построения специ- ализированныхрезультатов запросовLINQсаноним- ным типом, включающим в итоговую последователь- ность только нужные вам свойства. ¢ Запросы LINQ используют отложенное выполне- ние. Это означает, что они не выполняются до того момента, когда результаты запроса будут использо- ваться в команде. ¢ Ключевое слово new создает новый экземпляр ано- нимного типа, или объект с правильно определен- ным типом, не имеющим имени. Компоненты, ука- занные в команде new, становятся автоматическими свойствами анонимного типа. ¢ Используйте команду Rename в Visual Studio для удобного переименования всех вхождений имени пе- ременной, поля , свойства, класса или пространства имен. ¢ Используйте меню Quick Actions в Visual Studio для преобразования объявления var к явному типу или возврата к var (т. е. неявному типу). ¢ Один из самых распространенных приемов рефак- торинга — выделение методов. Функция Extract Method в Visual Studio позволяет очень легко преоб- разоватьблок кода в отдельный метод. ¢ Ключевое слово var может использоваться толь- ко при объявлении переменной. Вы не сможете напи- сать метод, который возвращает анонимный тип или получает его в параметре, а также использовать var с полем или свойством. Спасибо за помощь с коллекцией! Уж теперь я стану самым главным фанатом. КЛЮЧЕВЫЕ МОМЕНТЫ
540 глава 9 Модульные тесты помогают понять, как работает код Модульные тесты помогают понять, как работает код Мынамереннооставили ошибкув приведенномкоде...но являетсяли эта ошибка единственной? Написатькод, которыйделаетне совсемто, чтопредполагалось, очень просто.Ксчастью, суще- ствуютразличные способыпоиска ошибокдляих исправления.Модульные тесты(unittests) представляют собой автоматизированныетесты, которые проверяют, чтовашкодработает имен- нотак,какпредполагалось. Каждый модульный тестреализуетсяв виде метода, проверяющего работоспособность конкретной части кода. Если методвыполняется безвыдачи исключения, тест считаетсяпройденным. Если же при выполнении выдаетсяисключение,тест не проходит. Длямногихкрупныхпрограмм создаютсянаборытекстов, покрывающиебольшую часть кода. VisualStudioсодержитвстроенныесредстватестирования,которые помогаютписатьтестыи сле- дитьзатем, какиеизнихпроходятили не проходят.Длясозданиямодульныхтестовв этой книге будетиспользоватьсяMSTest—фреймворкмодульноготестирования(т.е .наборклассов,предо- ставляющихсредствадлянаписаниямодульныхтестов),разработанныйкомпаниейMicrosoft. Окно Test Explorer в Visual Studio для Windows Откройте окно Test Explorer командой View>>Test Explorer из главного меню. В левой части окна отображаютсямодульные тесты, а в правой —результаты их последнего выполнения. Кнопки панелиинструментовпредназначены для запуска всех тестов, запуска одного теста иповторения последнего запуска. Окно Unit Tests в Visual Studio для Mac Откройте окно Unit Tests командой меню View>>Tool Windows>>Unit Tests. Окно содержит кнопкидлязапуска или отладкитестов. Когда вызапускаетемодульные тесты,IDEвыводит результаты вокнеTestResults (обычно внижнейчасти окна IDE). В Visual Studio так- же поддер- живаются модульные тесты, на- писанные в NUnit и xUnit — двух популярных фреймвор- ках модуль- ного те- ст ир ова ния для кода C# и .NET, ра с- пространя- емых с от- крытым кодом. Когда вы добавляете тесты в решение, вы можете запустить тесты кнопкой Run All Tests. Отладка модульных тестов в Wi nd ows осущ ествл яет ся командой Tests>>Debug, а на Mac — кнопкой Debug All Tests в окне инструментов Un it Test s. Трассировка сте- ка помогает найти конкретную строку, в которой произошел сбой теста.
дальше 4 541 LINQи лямбда-выражения Тестирование Как только у вас появится работоспособный прототип игры, можно подумать о тестировании видеоигры. Если вы предо- ставите свою игру пользователям и получите от них обратную связь, это может в корне изменить ситуацию: в одном случае вы получаете игру, которая всем нравится, а в другом — поделку, которая раздражает новых пользователей, оставляя уних неудовлетворительные впечатления. Если вы когда-либо играли в игру, в которой вам было совершенно непонятно, что от вас нужно, или встречались головоломки, которые требовали сверхчеловеческих способностей, тогда вы знаете, что происходит в играх, которые не прошли достаточного игрового тестирования. Существует несколько подходов к тестированию игр, которые стоит рассмотреть, когда вы начнете проектировать и соз- даватьигры: • Индивидуальное игровое тестирование: предложите знакомым вам людям поиграть в игру(желательнопод вашим наблюдением). В самом неформальном варианте вы просто даете игру своему другу и обсуждаете его впечатления прямо по ходу игры. Если тестировщик будет постоянно говорить, что (по его мнению) от него хочет игра и что он думает об игровом процессе, это поможет вам спроектировать игру, которая нравится другим людям и будет до- ступной для них. Не предоставляйте тестировщикам слишком подробных инструкций и обращайте особое внимание, если у них возникнут проблемы. Это поможет вам оценить, понятна ли игроку суть игры, и осознать , что некоторые включенные в игру механики окажутся неочевидными для пользователя. Запишите все мнения, высказанные тести - ровщиками, чтобы вы могли исправить все выявленные недостатки. Получение обратной связи может осуществляться как неформально, так и в более формальной обстановке: вы со- ставляете список задач, которые предлагается выполнить пользователю, и анкету для получения обратной связи по игре. Зачастую обратную связь стоит запрашивать при добавлении новых возможностей, и вам стоит обращаться к людям с просьбой об игровом тестировании на самой ранней стадии разработки, чтобы проблемы с игрой обнару- живались на том этапе, когда ихбудет проще всего исправить. • Бета-версии: когда вы будетеготовы представить свой проектболее широкой аудитории, предложитебольшей группе людей поиграть в нее, прежде чем публиковать игру для всехжелающих. Бета-версии прекрасно подходят для выяв- ления потенциальных проблем с нагрузкой и быстродействием в игре. Обычно обратная связь от бета-тестирования анализируется через журналы. В журнале можно зарегистрировать время, потраченное пользователями на выполне- ние различных операций в игре, и по собранным данным выявить проблемы с выделением ресурсов в игре. Иногда при этом также выявляются области, недостаточно понятные для пользователей. Как правило, тестировщики подают заявки на участие в бета-тестировании, так что вы можете позднее расспросить их об игровом опыте и получить со- действие вдиагностике проблем, выявленных в журнальныхданных. • Структурированные тесты проверки качества: в большинстве игр существуют специализированные тесты, кото - рые выполняются как часть процесса разработки. Обычно эти тесты базируются на представлении о том, как должна работать игра, и они могут быть автоматизированными или ручными. Такое тестирование проводится для того, чтобы убедиться в том, что продукт работает так, как ожидалось. Идея проведения тестов проверки качества заключается в том, чтобы обнаружить как можно больше ошибок до того, как с ними столкнется пользователь . Ошибкирегистриру- ются с четкими пошаговыми инструкциями, чтобы их можно было воспроизвести и исправить. Затем они обрабатыва- ются в порядке убывания их влияния на впечатления игрока от игры и исправляются в соответствии с этим приорите- том. Если игра «падает» каждый раз, когда пользователь входит в комнату, вероятно, эту ошибку стоит исправитьдо некорректной прорисовки оружия в этой комнате. Многие команды разработки пытаются по возможности автоматизи- ровать процесс тестирования и запускать тесты перед каждым со- хранением изменений в кодовой базе. При таком подходе они могут быть уверены в том, что при исправлении или добавлении новой функциональ- ности не будут случайно внесены новые ошибки. В главе 3 вы узнали о прототипах — ранних версиях игр, в которые можно играть, тестировать, анализировать и совершенствовать. Вы узнали, что эта концепция применима к любым проектам, не только к играм. То же самое можно сказать о тестировании. Иногда идея те- стирования может показаться немного абстрактной. Изучение процесса тестирования разработчиками игр поможет вам привыкнуть к этой идее и сделает концепцию тестирования более понятной. Разработка игр... и не только
542 глава 9 модульные тесты обнаруживают ошибку Добавление проекта модульного теста в приложение Джимми Добавьте новый проект MSTest (.NET Core). Щелкнитеправой кнопкоймыши на именирешения вSolution Explorer,затем выберите команду Add>>NewProject... из меню. Проследите за тем, чтобыбыл выбран вариант MSTest Test Project (.NETCore): в Windows воспользуйтесь полем «Search for Templates» дляпоискаMSTest;вmacOS выберите вариантTests изкатегории«Web andConsole», чтобы увидеть шаблон проекта. Присвойте проекту имя JimmyLinqUnitTests. Добавьте зависимость от существующего проекта. Мы будем строить модульные тесты для класса ComicAnalyzer. Если одно решение включает два про- екта,эти проекты независимы — по умолчанию классы одного проекта немогутиспользовать классыдругого проекта, поэтомунеобходимо настроить зависимость, чтобы модульные тесты могли пользоваться классом ComicAnalyzer. Раскройте проектJimmyLinqUnitTests в окне Solution Explorer, затем щелкните правой кнопкой мыши на строкеDependencies ивыберите из меню командуAdd Reference....ВыберитепроектJimmyLinq, созданный для этого упражнения. Объявите класс ComicAnalyzer открытым. Когда среда VisualStudio добавила проектс модульны- ми тестами, она создала класс с именем UnitTest1. ОтредактируйтефайлUnitTest1.cs и по- пробуйте добавить директиву using JimmyLinq; в пространстве имен: Хм-м, что-тоне так:IDEне позволяет добавитьдирективу.Делов том, чтопроектJimmyLinq не содержит открытых классов,перечислений или другихкомпонентов. Попробуйте из- менить перечисление Critics, чтобыобъявить его открытым(publicenum Critics), затем вернитесь ипопробуйтедобавить директиву using.Теперь все получается!IDEвидит,что пространство именJimmyLinq содержит открытые компоненты, и добавляет его в окно. Теперь измените объявление ComicAnalyzer и добавьте модификатор public: public static class ComicAnalyzer. Исновачто-то нетак . Компилятор выдал ошибки «непоследовательныеуровни доступа»? Проблема в том, что класс ComicAnalyzer объявлен открытым, но он предоставляет ком- поненты, не имеющие модификаторов доступа, из-за чего они интерпретируютсякак вну- тренние(internal)— иостаютсяневидимымидля другихпроектовврешении. Добавьте модификатор доступаpublic ко всем классам и перечислениям проектаJimmyLinq. Теперьрешение снова строится нормально. 1 2 3 Вот почему мы предложили вам удалить модификатор доступа «public» из всех классов и перечис- лений проекта JimmyLinq — и вы сможете исследовать модификатор доступа «internal» в Visual Studio. В Windows и macOS окно Reference Manager в Visual Studio выглядит по- разному, но работает одинаково.
дальше 4 543 LINQи лямбда-выражения Первый модульный тест IDEдобавила впроектMSTestкласс с именем UnitTest1.Переименуйте класс(ифайл)иприсвойтеему имя ComicAnalyzerTests. Класс содержит тестовый метод с именем TestMethod1.Присвойте методу со- держательное имя: ComicAnalyzer_Should_Group_Comics. Код класса модульного теста выглядит так: usi ng M icro soft .Vis ualS tudi o.Te stTo ols. Unit Test ing; nam espa ce J immy Linq Unit Test s { us ing Jimm yLin q; us ing Syst em.C olle ctio ns.G ener ic; us ing Syst em.L inq; [T estC lass ] pu blic cla ss C omic Anal yzer Test s { I Enum erab le<C omic > te stCo mics = n ew[] { new Comic() { Issue = 1, Name = "Issue 1"}, new Comic() { Issue = 2, Name = "Issue 2"}, new Comic() { Issue = 3, Name = "Issue 3"}, }; [Te stMe thod ] pub lic void Com icAn alyz er_S houl d_Gr oup_ Comi cs() { var pric es = new Dic tion ary< int, dec imal >() { {1,20M}, {2,10M}, { 3, 1000M }, }; var grou ps = Com icAn alyz er.G roup Comi csBy Pric e(te stCo mics , pr ices ); Asse rt.A reEq ual( 2, g roup s.Co unt( )); Asse rt.A reEq ual( Pric eRan ge.C heap , gr oups .Fir st() .Key ); Asse rt.A reEq ual( 2, g roup s.Fi rst( ).Fi rst( ).Is sue) ; Asse rt.A reEq ual( "Iss ue 2 ", g roup s.Fi rst( ).Fi rst( ).Na me); } } } Запустите тест командой меню Test>>Run All Tests (Windows) или Run>>Run Unit Tests (Mac). IDE открывает окно Test Explorer (Windows) или панель Test Results (Mac) с результатами теста: Test method JimmyLinqUnitTests.ComicAnalyzerTests.ComicAnalyzer_Should_Group_Comics threw exception: System.Collections.Generic.KeyNotFoundException: The given key '2' was not present in the dictionary. Модульный тест не прошел. Найдите значок в Windows или сообщение в нижней ча - стиокна IDE вVisualStudio дляMaс — таквы определитеколичество непрошедшихмодульныхтестов. Авыожидали, что модульный тест не пройдет?Сможете ливы определить, что нетак с этим тестом? Когда вы запускаете модульные тесты, IDE ищет любой класс, перед которым стоит метка[TestClass]. Эта метка называется атрибутом. Класс теста включает тестовые методы, которыедолжны быть помечены атрибутом [TestMethod]. Модульные тесты MSTest использу- ют класс Assert, который содержит статические методы, проверяющие, что поведение вашего кода соответ- ствует ожиданиям. Данный модуль- ный тест использует метод Assert. AreEqual. Этот метод получает два параметра, ожидаемый результат (то, что , по вашему мнению, должен сделать код) и фактическийрезуль- тат (то, что он делает), и если они не равны — выдает исключение. Для этого теста создаются очень ограниченные тестовыеданные: по- следовательность из трех комиксов и словарь с тремяценами. Затем тест вызывает GroupComicByPrice и при помощи Assert.AreEqual про- в еряет, чт о резул ьтат соо тв ет ствует нашим ожиданиям. Переименуйте тесто- вый класс (и убедитесь в том, что файл так- же был переименован). Visual Studio добав- ляет эту директиву using над объявлением пространства имен. Взгляните повнимательнее на ожидаемые результаты. Тестовые данные состоят из 3 комиксов: 2сценойниже100и1сценой выше 100. Следовательно, для них должны быть созданы две группы: сначала группа с 2 дешевыми, за- тем группа с 1 дорогим комиксом. Группы сортируются по возрастанию цены, так что пер- вым элементом первой группы должен быть выпуск No 2. Присвойте содержательное имя тестовому методу.
исправление ошибки, обнаруженной в тесте Задача модульного тестирования — поиск мест , в которых ваш код работает не так, как ожидалось, и определение причин. Шерлок Холмс однажды сказал: «Худшая ошибка — строить теорию, не собрав данные». Начнем с тестовых утверждений Хорошей отправной точкой всегда становятся тестовые утверждения (assertions), включенные в тест , потому что они сообщают, какой конкретный код тестируется и какое поведение от него ожидается. Тестовыеутверждения из нашего модульного теста: Assert.AreEqual(2, groups.Count()); Assert.AreEqual(PriceRange.Cheap, groups.First().Key); Assert.AreEqual(2, groups.First().First().Issue); Assert.AreEqual("Issue 2", groups.First().First().Name); Если посмотреть на тестовые данные, поставляемые методу GroupComicByPrice, эти тестовые утверждения выгля- дят правильно. Они действительно должны возвращать две группы. Первая группа должна иметь ключ PriceRange. Cheap. Группы сортируются по возрастанию цены, так что первый комикс в первой группе должен иметь свойства Issue = 2 и Name = "Issue 2" — им е н н о эт о проверяют наши тестовые утверждения. Таким образом, если возникает проблема, она кроется не здесь — эти тестовые утверждения выглядят правильными. Обратимся к трассировке стека Вы уже видели много исключений. К каждому исключению прилагается трассировка стека — список всех вызовов методов, сделанных программой непосредственно в той строке, в которой это исключение произошло. Для методов указывается, в какой строке кода был вызван этот метод, из какой строки был вызван предыдущий, и так далее вплоть до метода Main. Откройте трассировку стека для непрошедшего модульного теста: • Windows: откройте окно Test Explorer (View>>Test Explorer), щелкните на тесте и прокрутите результаты до Test Detail Summary. • Mac: перейдите на панель Test Results, раскройте тест, а затем раскройте в его описании раздел Stack Trace. Трассировка стека начинается примерно так (на Mac будут использоваться полные имена классов вида JimmyLinq. ComicAnalyzer): at Di ction ary`2 .get_ Item( TKey key) at Ca lcula tePri ceRan ge(Co mic c omic) in Co micAn alyze r.cs: line 11 at <> c.<Gr oupCo micsB yPric e>b__ 1_1(C omic comic) in C omicA nalyz er.cs :line 22 Трассировка стека поначалу выглядит немного странно, но когда вы привыкнете, она может предоставить много по- лезной информации. Теперь вы знаете, что тест не прошел из-за того, что где-то внутри CalculatePriceRange было выдано исключение, связанное с ключами словаря. Используйте отладчик для сбора информации Установите точку прерывания в первой строке метода CalculatePriceRange:if (Comic.Prices[comic.Issue] < 100) Затем проведите отладку модульных тестов: в Windows выберите командуTest>>DebugAllTests, на Mac открой- те панель Unit Tests (View>>Tests) и щелкните на кнопке Debug All Tests в верхней части. Наведите указатель мыши на comic.Issue — значение равно 2. Минутку! В словаре Comic.Prices нет элемента с ключом 2. Неудивительно, что в программе произошло исключение! Теперь становится ясно, как исправить ошибку: • Добавьте в метод CalculatePriceRange второй параметр: private static PriceRange CalculatePriceRange(Comic comic, IReadOnlyDictionary<int, decimal> prices) • Измените первую строку, чтобы в ней использовался новый параметр: if (prices[comic.Issue] < 100) • Измените запросLINQ: group comic by CalculatePriceRange(comic, prices) into priceGroup Снова запустите тест. На этот раз он проходит успешно! По сле ду Щелкните (Windows) или сделайте двойной щелчок (Mac) на трасси- ровке стека, чтобы перейти к коду.
дальше 4 545 LINQи лямбда-выражения Написание модульного теста для метода GetReviews В модульном тесте для метода GroupComicsByPrice использовался статический метод MSTest Assert. AreEqual для сравнения ожидаемых значений с фактическими. Метод GetReviews возвращает по- следовательностьстрок, а неотдельноезначение.ТеоретическиможноиспользоватьAssert.AreEqual длясравненияотдельныхэлементовэтойпоследовательности,какэто делалось вдвухпоследних тестовых утверждениях, используя методы LINQ(такие, как First) для получения конкретных элементов... но такое решение получитсяОЧЕНЬ длинным. К счастью, в MSTest предусмотрен более эффективный способ сравнения коллекций: класс CollectionAssert содержит статические методы для сравнения ожидаемых результатов-коллек- цийс фактическими. Таким образом,если у вас имеется коллекция с ожидаемыми результатами иколлекция с фактическимирезультатами, ихможно сравнить так: CollectionAssert.AreEqual(expectedResults, actualResults); Еслиожидаемыйрезультатнесовпадаетс фактическим,тестнепроходит. Добавьте следующий тест в метод ComicAnalyzer.GetReviews: [TestMethod] public void ComicAnalyzer_Should_Generate_A_List_Of_Reviews() { var testReviews = new[] { new Review() { Issue = 1, Critic = Critics.MuddyCritic, Score = 14 .5}, new Review() { Issue = 1, Critic = Critics.RottenTornadoes, Score = 59.93}, new Review() { Issue = 2, Critic = Critics.MuddyCritic, Score = 40 .3}, new Review() { Issue = 2, Critic = Critics.RottenTornadoes, Score = 95.11}, }; var expectedResults = new[] { "MuddyCritic rated #1 'Issue 1' 14.50" , "RottenTornadoes rated #1 'Issue 1' 59.93", "MuddyCritic rated #2 'Issue 2' 40.30" , "RottenTornadoes rated #2 'Issue 2' 95.11", }; var actualResults = ComicAnalyzer.GetReviews(testComics, testReviews).ToList(); CollectionAssert.AreEqual(expectedResults, actualResults); } Сновазапустите тесты. На этотраз два модульныхтеста должны пройти. Что п роизойдет, есл и п ередать C omi c Anal yz er. GetR evi ew s п оследов атель н ость с д ублик атами обзоров ? А есл и п еред ать обзор с отрицатель н ой оц енк ой? Мозговой штурм
546 глава 9 обработка аномальныхданных Написание модульных тестов для обработки граничных случаев и аномальных данных Вреальноммире данные не идеальны.Например, мы нигде точноне определяли, какдолжны выглядеть данные обзоров.Вывиделиоценки от0 до 100.Вы посчитали,что допустимы только значенияиз этого диапазона? В реальном мире на сайтах с обзорами это определенно так. А если вы получите обзоры с аномальнымиоценками— например, отрицательными,илиочень большими,илинулевыми?Есливы получитеболееоднойоценки отконкретногорецензента дляконкретноговыпуска?Дажеесливсеэти аномалии не должны встречаться, онимогутвстретиться. Код должен быть защищенным от ошибок; это означает, что он должен успешно обрабатывать про- блемы,сбоии особенно некорректныевходные данные. Построим модульныйтест, которыйпередает GetReviews некорректные данныеи убеждаетсяв том, что онине нарушаютработоспособность кода: [Tes tMet hod] p ubli c vo id C omic Anal yzer _Sho uld_ Hand le_W eird _Revi ew_S core s() { var testReviews = new[] { new Review() { Issue = 1, Critic = Critics.MuddyCritic, Score = -12 .1212}, new Review() { Issue = 1, Critic = Critics.RottenTornadoes, Score = 391691234.48931}, new Review() { Issue = 2, Critic = Critics.RottenTornadoes, Score = 0}, new Review() { Issue = 2, Critic = Critics.MuddyCritic, Score = 40.3}, new Review() { Issue = 2, Critic = Critics.MuddyCritic, Score = 40.3}, new Review() { Issue = 2, Critic = Critics.MuddyCritic, Score = 40.3}, new Review() { Issue = 2, Critic = Critics.MuddyCritic, Score = 40.3}, }; var expectedResults = new[] { "MuddyCritic rated #1 'Issue 1' -12 .12" , "RottenTornadoes rated #1 'Issue 1' 391691234.49", "RottenTornadoes rated #2 'Issue 2' 0.00", "MuddyCritic rated #2 'Issue 2' 40.30", "MuddyCritic rated #2 'Issue 2' 40.30", "MuddyCritic rated #2 'Issue 2' 40.30", "MuddyCritic rated #2 'Issue 2' 40.30", }; v ar a ctua lRes ults = C omic Anal yzer .Get Revi ews( test Comi cs, test Revi ews) .ToL ist( ); C olle ctio nAss ert. AreE qual (exp ecte dRes ults , ac tual Resu lts) ; } А если одни и те же данные встречаются не- сколько раз? Вроде бы очевидно, что код должен успешно обработать их, но это не означает , что они действительно будут обработаны. Справится ли наш код с от- рицательными числами? Очень большими числами? Нулями? Такие случаи прекрасно подходят для модульных тестов. Метод GetReviews возвращает по- следовательность строк и округляет оценку до двух знаков в дробной части. Очень важно добавить модульные тесты для обработки граничных случаев и аномальных данных. Они помогут выявить проблемы в коде, которые можно было бы упустить в других ситуациях. Всегда пишите модульные тесты для граничных случаев и аномальныхданных — считайте, что это попросту обязательно. Модульноетекстиро- вание должно раскинуть свою сеть для выявле- ния ошибок как можно шире, и такие тесты очень эффективно работают в этом отношении.
дальше 4 547 LINQи лямбда-выражения В: Почему тесты называются модуль- ными? О: «Модульный тест» — общий термин, используемый во многих языках, не только в C#.Он происходит от представления о том, что код делится на отдельные структурные единицы, называемые модулями. Вразных языках используютсяразные единицы; в C# базовой единицей считается класс. С этой точки зрения термин «модульный тест» выглядит логично: вы пишете тесты дляотдельных модулей, или в нашем слу- чае — для отдельных классов. В:Я создал два проекта в одномреше- нии. Как это работает? О: Когда вы делаете новый проект C# в Visual Studio, среда создает решение и добавляет в него проект. Все решения, описанные до настоящего момента в книге, состояли из одного проекта — до проек- та с модульнымитестами. На самом деле реше ние может с оде ржать мног о проек тов. Мы использовали отдельный проект для того, чтобы отделить модульные тесты от тестируемого кода. Также можно добавить неск оль ко конс оль ных п ри лож ений, п роектов ASP.NET или WPF — решение может со- держ ат ь к омбинац ию раз нотипны х пр оектов . В: Если решение содержит несколько проектов консольных приложений, WPF или ASP.NET, как IDE определяет, какой проект должен запускаться? О: ВзглянитенаокноSolutionExplorer(или панель Solution в VSдля Mac) — одно из имен проектов выделено жирным шрифтом. IDE на- зывает его стартовым проектом. Вы можете щелкнуть правой кнопкой мыши на любом проекте в решении и сообщить IDE, что этот проект должен использоваться в качестве стартового. В этом случае при следующем нажатии кнопки Runна панели инструментов будетзапущен выбранный вами проект. В: Ещераз — как работает модификатор доступаinternal? О:Когда вы помечаете класс или интерфейс модификатором internal, это означает, что к ним можно обращаться только из кода этого проек- та. Если модификатор доступа вообще не ука- зан, для класса или интерфейса по умолчанию используется модификатор internal. Именно по этой причине вам приходилось следить за тем, чтобы ваши классы имели пометкуpublic, в про- тивном случае модульныетесты ихне увидят. Такжебудьте осторожны с модификаторамидо- ступа: хотя при отсутствии модификатора клас- сы и интерфейсы по умолчанию используют internal, для компонентов классов — методов, полей и свойств —по умолчанию используется модификатор private. В: Еслимои модульныетесты хранят- ся в отдельном проекте, как они могут обратиться к приватным методам тести- руемого кода? О: Они не могут. Модульные тесты об- ращаются к части, видимойдля остального кода (для ваших классов C# это открытые методы и поля), и используют их, чтобы убедиться в работоспособности модуля. Модульныетестыобычнодолжны работать по принципу «черного ящика», это означа- ет, что они проверяют только видимые им методы (в отличие от тестов, работающих по принципу «белого ящика», которые «ви- дят» внутреннюю реализацию тестируемого модуля). В:Всемои тесты должны проходить? А если какие-то тесты не проходят, это критично? О:Да, все тесты должны проходить. Если какие-то тесты не проходят, это совершенно недопустимо. Взгляните на происходящее так: если какая-то часть вашего кода не ра- ботает, это нормально?Конечнонет!Таким образом, если ваш тест не проходит, зна- чит, ошибка присутствует либо в коде, либо в тесте. В любом случае ошибку следует исправить, чтобы тест проходил. Кажется, написание тестов требует немалой лишней работы. Разве не быстрее просто написать код и обойтись без модульных те ст ов? На самом деле при написании модульных тестов ваши проекты идут быстрее. Серьезно!Тотфакт, что длянаписаниябольшегообъемакода требуетсяменьше времени, вы- глядит противоестественно, но если выпривыкнете писать модульные тесты, вашипроекты пойдут заметнобыстрее,потомучтоошибкибудут обнаруживатьсяиисправлятьсянаранней стадии. Впервыхвосьми с половиной главах этой книги вы написали большой объем кода, а это означает, что вам почти наверняка приходилось отслеживать и исправлять ошибки в своемкоде. Когда вы исправляли эти ошибки, не приходилось ли вамисправлять другой код в своемпроекте?Когдав коде проявляетсянеожиданнаяошибка,вамчастоприходится бросать то, чем вы занимаетесь, чтобы найти и исправить ошибку.Подобные переключе- ния — потеря хода мысли, необходимость прерыватьсяи братьсяза другую задачу — могут серьезнозамедлить ход работы.Модульные тестыпомогаютнаходить подобные ошибки на ранней стадии, прежде чем им представится возможность прервать вашуработу. Всееще размышляете над тем, когда следует заниматься написанием модульных тестов? Вконцеглавымы приводимпроект, загружаемыйпо ссылке; он поможет вам получитьответ на этот вопрос. часто Задава ем ые вопросы
548 глава 9 лямбда-выражения как анонимные функции Оператор => и создание лямбда-выражений Всамом началеглавыодин вопрос осталсябез ответа. Помните загадочную строку,которую мы пред- ложили добавить в класс Comic? Приведем ее еще раз: public override string ToString() => $"{Name} (Issue #{Issue})"; ВынеразиспользовалиэтотметодToString вглаве— значит,онработает.А чтобывысделали,если бымыпредложилипереписать этотметод втом виде,в каком вы записывали методыдо этого? Вы бы написали что-нибудь такое: public override string ToString() { return $"{Name} (Issue #{Issue})"; } Иэтобылобы правильно. Что жепроисходит?Что делает оператор =>? Оператор =>,который вы использовали в методе ToString,называется лямбда-оператором. Онис- пользуется для определения лямбда-выражений, или анонимных функций, определяемых в одной команде. Лямбда-выражения выглядят так: (входны е-па раме тры) => в ыраж ение; Лямбда-выражениесостоитиз двухчастей: ≥ Входные-параметры— списокпараметров, аналогичных тем, которыеиспользуются приобъ- явленииметода. Еслипараметр только один,круглыескобкиможно опустить. ≥ Выражение — произвольноевыражениеC#; это можетбыть интерполируемаястрока, команда с оператором,вызовметода — практическивсе,что можетсодержать команда. Лямбда-выраженияна первыйвзглядвыглядятстранно,но на самом делеэто всего лишьдругойспо- соб использованияуже известныхвам выраженийC#,которымивыпользовались вкниге, — по сути, это тот же метод Comic.ToString, которыйработает одинаково независимо от того, используется лям бда - выр а жение или нет. Если метод ToString работает одинаково независимо от того, используется лямбда- выражение или нет, получается, что мы провели рефакторинг? Да! Лямбда-выражения могут использоваться для рефакторинга многих методов и свойств. Вэтой книге вы написали много методов, состоящих всего из однойкоманды.Большинство из нихможно подвергнутьрефак- торингуииспользовать вместо исходнойреализациилямбда-вы - ражения. Во многих случаях этоупроститчтение ипонимание кода.Лямбда-выражениярасширяют вашу свободу выбора — вы можетесами решить, когда ихиспользованиеулучшает вашкод.
дальше 4 549 LINQи лямбда-выражения Создайте новое консольное приложение. ДобавьтеклассProgram с методом Main: class Program { static Random random = new Random(); static double GetRandomDouble(int max) { return max * random.NextDouble(); } static void PrintValue(double d) { Console.WriteLine($"The value is {d:0.0000}"); } static void Main(string[] args) { var value = Program.GetRandomDouble(100); Program.PrintValue(value); } } Выполните его несколько раз. Каждый раз будет выводиться новое случайное число — на - пример,The value is 37.8709 Проведите рефакторинг методов GetRandomDouble иPrintValue с использованием оператора =>: static double GetRandomDouble(int max) => max * random.NextDouble(); static void PrintValue(double d) => Console.WriteLine($"The value is {d:0.0000}"); Сновазапустите программу—она должнавывести новоеслучайное число,каки в предыдущем варианте. Прежде чем продолжать рефакторинг, наведите указатель мыши на поле random и озна- комьтесь с содержимым окнаIntelliSense: Изменитеполеrandom для использования лямбда-выражения: static Random random => new Random(); Программа все еще работает так же. Снова наведите указатель мышина поле random: Один момент — random уже не является полем. При преобразованиив лямбда-выражении поле превратилось в свойство!Деловтом, что лямбда-выражения всегда работаюткак методы. Пока значение random оставалось полем, его экземпляр создавался при выполнениикон- структора класса. Когда вы заменяете = на =>ипреобразуетереализацию влямбда- выражение,randomстановитсяметодом, аэтоозначает, чтоновыйэкземпляр Randomсоздается при каждом обращениик свойству. 1 2 3 Тест-драйв лямбда-выражений Опробуемна практикелямбда-выражения— новый способ записимето- дов,включаяте, которыевозвращаютзначенияили получают параметры. Как вы думаете, к акая из версий кода проще читается? Мозговой штурм Это не настоящий рефакторинг, потому что мы изменили поведение кода, а не только его структуру. Каждое из этих лямбда- выражений получает параметр — как и метод, который они заменяют.
550 глава 9 мы думали, что клоуны уже не вернутся Рефакторинг клоунов с использованием лямбда-выражений Вглаве7 мысоздалиинтерфейсIClownс двумякомпонентами: interface IClown { string FunnyThingIHave { get; } void Honk(); } Классбылизменен дляиспользования этого интерфейса: class TallGuy { public string Name; public int Height; public void TalkAboutYourself() { Console.WriteLine($"My name is {Name} and I'm {Height} inches tall. "); } } Проделаем то же самое снова — но на этот раз с лямбда-выражениями. Создайте проектнового кон- сольногоприложения,добавьте интерфейсIClownи TallGuy.ИзменитеTallGuyдляреализации IClown: class TallGuy : IClown { Теперь откройтеменюQuickActions и выберитекоманду«Implement interface». IDEзаполняетвсе компоненты интерфейса, чтобы они выдавали исключение NotImplementedException, как это дела- лось при использованииGenerate Method. public string FunnyThingIHave => throw new NotImplementedException(); public void Honk() { throw new NotImplementedException(); } Проведем рефакторинг методов, чтобы они делали то же самое, но с использованием лямбда-вы - ражений: public string FunnyThingIHave => "big red shoes"; public void Honk() => Console.WriteLine("Honk honk!"); Добавим методMain,использованныйв главе7: TallGuy tallGuy = new TallGuy() { Height = 76, Name = "Jimmy" }; tallGuy.TalkAboutYourself(); Console.WriteLine($"The tall guy has {tallGuy.FunnyThingIHave}"); tallGuy.Honk(); Запуститеприложение. КлассTallGuyработаеттакже, какв главе 7, нопосле рефакторингаметодовс лямбда-выражениямикодсталболеекомпактным. Мысчитаем, что новый улучшенныйкласс TallGuyпроще читается. Авыкак думаете? Когда вы приказали IDE реализовать интерфейс IClown, среда воспользовалась оператором => для создания лямбда- выражения, реализующего свойство. Интерфейс IClown из главы 7 содержит два компонента: одно свойство и один метод. Когд а клас с реали зует и нтерф ейс , команда меню QuickActions «Implement Interface» приказывает IDE добавить все недос таю щ ие ком понен ты ин терф ейс а. Подсказки для IDE: реализация интерфейсов СредаIDE создала тело метода при добавлении компонентов интерфейса, но вы можете заменить его лямбда-выражением. У таинственного метода ToString в начале главы также использо- валось лямбда-вы- ражение в каче- стве тела. Сделайте это!
дальше 4 551 LINQи лямбда-выражения Ниже приведен класс NectarCollector из проекта системы управле- ния ульем (глава 6) и класс ScaryScary (глава 7). Ваша задача — провести рефакторинг некоторых компонентов клас- са с использованием лямбда-оператора (=>). Запишите полученные методы. class NectarCollector : Bee { public const float NECTAR_COLLECTED_PER _SHIFT = 33 .25f; public override float CostPerShift { get { return 1.95f; } } public NectarCollector() : base("Nectar Collector") { } protected override void DoJob() { HoneyVault.CollectNectar(NECTAR_COLLECTED _PER_SHIFT); } } Проведите рефакторинг свойства CostPerShift в форме лямбда-выражения: class ScaryScary : FunnyFunny, IScaryClown { private int scaryThingCount; public ScaryScary(string funnyThing, int scaryThingCount) : base(funnyThing) { this.scaryThingCount = scaryThingCount; } public string ScaryThingIHave { get { return $"{scaryThingCount} spiders"; } } public void ScareLittleChildren() { Console.WriteLine($"Boo! Gotcha! Look at my {ScaryThingIHave}"); } } Проведите рефакторинг свойства ScaryThingProperty в форме лямбда-выражения: Проведите рефакторинг метода ScareLittleChildren в форме лямбда-выражения: Возьми в руку карандаш
552 глава 9 подробнее о лямбда-выражениях Ниже приведен класс NectarCollector из проекта системы управления ульем (глава 6) и класс ScaryScary (глава7).Ваша задача — провести рефакторинг некоторых компонентов класса с использо- ванием лямбда-оператора(=>). Запишите полученные методы. Проведите рефакторинг свойства CostPerShift в форме лямбда-выражения: Проведите рефакторинг свойства ScaryThingProperty в форме лямбда-выражения: Проведите рефакторинг метода ScareLittleChildren в форме лямбда-выражения: public float CostPerShift { get => 1.95f; } public string ScaryThingIHave { get => $”{scaryThingCount} spiders”; } public void ScareLittleChildren() => Console.WriteLine($”Boo! Gotcha! Look at my {ScaryThingIHave}”); В: Вернитесь к тест-драйву, где мы изменяли поле random. Вы сказали, что это «не настоящий рефакто- ринг»,— можно пояснить,чтовыимеливвиду? О: Конечно. Прирефакторинге кода вы изменяете его струк- туру без изменения поведения. Когда мы преобразовали методы PrintValue и GetRandomDouble в лямбда-выражения, они работали абсолютно так же — изменилась структура, но изме нение ника к не повлия ло на повед ение. Но когда в объявлении поля random знак равенства (=) был преобра зов ан в лямб да-о перато р ( => ), из менило сь пов еде ние. Поленапоминаетпеременную — вы одинраз объявляете его, а потом используете повторно. Таким образом, когда random было полем, новый экземпляр Random создавался при за- пуске программы и ссылка на этот экземпляр сохранялась в поле ra ndom. Но при использовании лямбда-выражения всегда создается метод. Таким образом, когда вы заменили поле random сле- д ую щим объ явле нием: static Random random => new Random(); компилятор C# уже не видит поле. Вместо него он видит свой- ство. И это логично — в главе 5 вы узнали, что к свойствам вы обращаетесь как к полям, но в действительности они являются ме тод ами. А чтобы убедиться в этом, достаточно навести указатель мыши на поле: IDE сообщает, что random теперь является свойством, по - казывая, что у него теперь есть get-метод { get;}. Оператор => может использоваться для создания свойства с get-методом, выполняющим лямбда-выражение. часто Зад авае м ые вопросы Возьми в руку карандаш Решение
дальше 4 553 LINQи лямбда-выражения Использование оператора ?: для принятия решений в лямбда-выражениях А есливы хотите, чтобы лямбда-выражение делало... нечто большее?Былобы здорово, если бы они могли принимать решения... и в этом вам поможет условный оператор(который иногда называется тернарным оператором).Онработаетпримерно так: условие ? следствие : альтернатива; Выражениевыглядит немного странно,поэтомурассмотрим пример.Преждевсего оператор ?: рабо- тает не только с лямбда-выражениями — они могут использоваться где угодно. Возьмем команду if из класса AbilityScoreCalculator вглаве 4: if (added < Minimum) Score = Minimum; els e Score = added; Командуможно переработать с использованием оператора ?:, результат выглядит примерно так: Score = (added < Minimum) ? Minimum : added; Обратитевниманиенато,что Score присваиваетсярезультат выражения?:. Выражение?: возвращает значение: оно проверяетусловие(added < Minimum), а затем возвращает либо следствие(Minimum), либ о альте р натив у (a dded). Еслиувасимеетсяметод, которыйработает пологикеif/else, выможете воспользоватьсяоператором ?: длярефакторинга еговлямбда-выражение.Для примеравозьмем методизкласса PaintballGun изглавы5: public void Reload() { if (balls > MAGAZINE_SIZE) BallsLoaded = MAGAZINE_SIZE; else BallsLoaded = balls; } Перепишем этоткод ввиде более лаконичного лямбда-выражения: public void Reload() => BallsLoaded = balls > MAGAZINE_SIZE ? MAGAZINE_SIZE : balls; Обратитевниманиенанебольшое изменение —в версииif/elseсвойствоBallsLoaded задавалось в обеих ветвях. Мы воспользовалсь условным оператором, который сравниваетMAGAZINE_SIZE, возвращает правильноезначениеииспользуетего для задания свойстваBallsLoaded. Простоеправило поможет запомнить, какработаетусловныйоператор. Многимлюдям трудно запомнить порядок? и : в тернарном операторе?:. К счастью, простое мнемоническое правило поможет запомнить структуруоператора. Оператор словно задает вопрос,а вопрос всегда задаетсядополучения ответа. Спроситесебя: iусловие истинно ? да : нет Итогда становится очевидно, что ?предшествует : ввыражении. Забавныйфакт:мыузналиоб этом изпревосходнойдокументацииMicrosoftс описаниемоператора?: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/conditional-operator Эта условная команда «if» в случае истинности ус- ловия (balls > MAGAZINE_SIZE) выполняет основную ветвь (BallsLoaded = MAGAZINE_SIZE), а в случае лож- ности выполняет ветвь else (BallsLoaded = balls). Мы преобразовали «if» в компонент, тело которого определяется выражением (в операторе ?: как след- ствие, так и альтернатива возвращают значение), и использовали его для задания свойства BallsLoaded. Выражение ?: проверя- ет условие (added < Minumum). Если условие истинно, выра- жение возвра- щает значение Minimum. В противном случае воз- вращается added. РЕ Л А К С
554 глава 9 использование сцепленных вызовов для записи запросовlinq Лямбда-выражения и LINQ Добавьте этот небольшой запрос LINQ влюбое приложениеC#, затем наведите указатель мыши на ключевоеслово select вкоде: Принаведении указателя мыши на метод IDE открывает окно с подсказкой. Присмотритесь к первой строке,в которой выводитсяобъявление метода: IEnu merab le<in t> IE numer able< int>. Selec t<int, int> (Func <int, int> sele ctor) Изобъявления метода можно узнать кое-что полезное: ≥ Метод IEnumerable<int> Select возвращает IEnumerable<int>. ≥ Онполучаетодин параметр типаFunc<int,int>. Использование лямбда-выражений с методами, получающими параметр Func Еслиметодполучает параметрFunc<int, int>, его можно вызватьслямбда-вы- ражением, которое получает параметрint и возвращает значение int. Таким образом,запрос selectможно переписать вследующем виде: vararray=new[]{1,2,3,4}; var result = array.Selec t(i => i * 2); Убедитесь в этом сами в консольном приложении. Добавьтекомандуforeachдлявывода результата: foreach (var i in result) Console.WriteLine(i); При выводе результатов переработанного запроса вы получитепоследовательность {2,4,6,8}— точно такой жерезультат,какс синтаксисомзапросаLINQ до его рефакторинга. Выглядит как объявление метода — такое же, какое ото- бражается при наведе- нии указате- ля мыши на любой другой метод. Метод IEnumerable<int> Select получает один параметр типа Func<int, int>; это означает, что вы можете использо- вать лямбда-вы- ражение, которое получает параметр int и возвращает int. Каждыйраз, когда вывидите Funcвметоде LINQ, это озна - чает, что здесь может исполь- зоваться лямбда-выражение. ОFunc намногоподробнеерассказано в главе, посвященной событиям и делегатам, до- ступной в видеPDF-файла настранице GitHub. А пока скажем, что при использовании пара- метра метода LINQ с типом Func<TSource, TResult> метод можно вызвать с передачей ему лямбда-выражения, которое получает па- раметртипаTSource ивозвращает тип TResult. РЕ Л А К С
дальше 4 555 LINQи лямбда-выражения Запросы LINQ могут записываться в виде сцепленных вызовов методов LINQ ВозьмитезапросLINQ, которыйбылприведен ранее, идобавьтеегов приложение, чтобыисследовать другие методы LINQ: int[] values = new int[] { 0, 12, 44, 36, 92, 54, 13, 8 }; IEnumerable<int> result = from v in values wherev<37 orderby -v select v; Метод LINQ OrderBy сортирует последовательность Наведите указатель мыши на ключевое словоorderbyи обратите внимание на его параметр: Когдавы используетесекцию orderby взапросеLINQ,она вызывает метод LINQOrderBy,которыйсор- тируетпоследовательность. В данномслучае ему можно передать лямбда-выражениес параметромint, котороевозвращаетключсортировки илилюбоезначение(котороедолжнореализоватьIComparer), котороеможетиспользоватьсядлясортировкирезультатов. Метод LINQ Where выделяет подмножество последовательности Наведите указатель мышина ключевоеслово where в запросеLINQ: Секция where в запросе LINQ вызывает метод LINQ Where, который использует лямбда-выражение ивозвращает bool.Метод Where вызывает лямбда-выражениедля каждого элемента в последовательности. Еслилямбда-выражение возвращает true, то элемент включается в результат. Если возвращается false, то элементисключается. Упражнение по работе с лямбда-выражениями! А вы сможете провести рефакторинг этого запроса LINQ и пре- образовать его в запрос сцепленных вызовов методов LINQ? Начните с result, присоедините методы Where и OrderBy дляполучения той же последовательности. IEnumerable<int> result = from v in values wherev<37 orderby -v select v; Последовательность int со- держит метод OrderBy с лямбда-выражением, которое получает int и возвращает int. Он работает так же, как и методы сравнения, описанные в главе 8. Метод LINQ Where использует лямбда- выражение, которое получает элемент последовательности и возвращает true, если элемент включается в результат, или false, если его следует пропустить. Упражнение M I N I
556 глава 9 выражения switch возвращают значения Запросы LINQ можно переписать в виде серии сцепленных вызовов методов LINQ, и многие из этих методов могут использовать лямбда-выражения для определения ге- нерируемых последовательностей. Решение для мини-упражнения — запрос LINQ преобразуется к следующему виду: var result = values.Where(v => v < 37).OrderBy(v => -v); Разберемся, как же запрос LINQ преобразуется в сцепленные вызовы методов: IEnumerable<int> result = from v in values wherev<37 orderby -v select v; var result = values .Where(v=>v<37) . OrderBy(v => -v); Используем var для объявления переменной Начинаем споследовательности values Вызываем Whereс лямбда-выражением,включающим значения <37 Вызываем OrderBy слямбда-выражением,меняющим знак значения Метод.Select ненужен,потому что секция select неизменяет значения Используйте метод OrderByDescending в тех ситуациях, когда бы вы использовали ключевое слово descending в запросе LINQ Помните, как вы использовали ключевое слово descending для изменения секции orderby в запросе? Суще- ствует эквивалентный метод LINQ OrderByDescending, который делает то же самое: var result = values.Where(v => v < 37).OrderByDescending(v => v); Обратите внимание на использование лямбда-выражения v => v — это лямбда-выражение всегда возвра- щает переданное ему значение (оно иногда называется тождественным отображением). Таким образом, OrderByDescending(v => v) меняет порядок элементов на противоположный. Используйте метод GroupBy для создания запросов с группировкой из сцепленных методов Следующий запрос с группировкой был приведен ранее в этой главе: var grouped = from card in deck group card by card.Suit into suitGroup or derby suit Group .Key desce nding se lect suitG roup; Наведите указатель мыши на ключевое слово group. Вы увиди- те, что оно вызывает метод LINQGroupBy, возвращающий тот же тип, который был приведенранее в этой главе. Вы може- те воспользоваться лямбда-выражением для группиров- ки результата по масти карты: card => card.Suit Затем другое лямбда-выражение используется для упорядочения групп по ключу: group => group.Key. Запрос LINQ, преобразованный посредством рефакторинга в сцепленные вызовы методов GroupBy и OrderByDescending: var grouped = deck.GroupBy(card => card.Suit) .OrderByDescending(group => group.Key); Попробуйте вернуться к приложению, в котором вы использовали этот запрос, и заменить его сцепленными вызовами методов. Вы получите точно такой же результат. Сами решите, какая версия кода более понятна и проще читается. Какой вариант использовать: синтаксис де- кларативных запросов LINQ или сцепленные вызовы? Оба варианта приводят к одному результату. Иногда первый вариант упрощает чтение кода, иногда — второй; выдолжны уметь пользоваться обеими формами записи. Код под увеличительным стеклом Упражнение Решение MI N I
дальше 4 557 LINQи лямбда-выражения Использование оператора => для создания выражений switch Командыswitchиспользовались, начинаяс главы6, дляпроверкизначенияпеременнойпонескольким возможным вариантам. Этополезныйинструмент...но вызаметили, что еговозможностиограниченны? Например, попробуйте добавить альтернативу case для сравнения с переменной: case myVariable: КомпиляторC#выдает ошибку: «Ожидаетсяконстанта». Деловтом, что вкомандахswitch, которымивы пользовались,могутиспользоваться только значения-константы — например,литералы и переменные, определенныес ключевым словом const. Новсеменяетсясоператором =>,который позволяет создавать выражения switch.Они имеютмного общегос командамиswitch,которымивы пользовались,ноприэтомявляютсявыражениями,которые возвращают значение.Выражение switch начинаетсяспроверяемогозначенияиключевого слова switch, за которым следует серия ветвей switch вфигурных скобках, разделенных запятыми. Каждая ветвь ис- пользует оператор =>для сравнения проверкизначенияпо выражению. Еслипервая ветвь неподходит, происходитпереход кследующей; витоге возвращаетсязначение для подходящей ветви. var returnValue = valueToCheck switch { pattern1 => returnValue1, pattern2 => returnValue2, ... _ => defaultReturnValue, } Допустим, выработаетенадкарточнойигрой, котораядолжна при- сваивать определенное количествоочков в зависимостиотмасти;за пикиначисляется6очков, зачервы— 4очка, а все остальные карты приносят2 очка.Можнонаписать командуswitchследующеговида: var score = 0; switch (card.Suit) { case Suits.Spades: score = 6; break; case Suits.Hearts: score = 4; break; default: score = 2; break; } Единственная цель этой команды switch — присваивание значения переменной score. Очень многие командыswitchработают поэтойсхеме.Воспользуемсяоператором =>длясозданиявыраженияswitch, который делает то же самое: var score = card.Suit switch { Suits.Spades => 6, Suits.Hearts => 4, _ => 2, }; В каждой секции case этой команды switch присваивается значение переменной score. Такая команда становится отличным кандидатом для преобразования в выражение switch. Это выражение switch проверяет значение card.Suit — если оно равно Suits.Spades, то выражение возвращает 6; если оно равно Suits.Hearts, то возвращается 4; во всех остальных случаях возвращается 2. Выражение switch начинается с про- веряемого значения, за которым следует ключевое слово switch. Тело выражения switch представ- ляет собой серию ветвей, кото- рые используют оператор => для проверки valueToCheck и возвра- щают значение при совпадении. Выражения switch должны быть исчерпывающими, т. е. их условиядолжны покрывать все возможные значения. Шаблон _ подходит для любых значений, которые не совпали с остальными ветвями.
558 глава 9 еще немного потренируемся с linq В этом консольном приложении используются классы Suit, Value и Deck, которые встречались вам в этой главе. Приложение выво- дит шесть строк на консоль. Ваша задача — записать результат выполнения программы. Когда это будет сделано, добавьте про- грамму в консольное приложение и проверьте свой ответ. class Program { static string Output(Suits suit, int number) => $"Suit is {suit} and number is {number}"; static void Main(string[] args) { var deck = new Deck(); var processedCards = deck .Ta ke (3) .Concat(deck.TakeLast(3)) .OrderByDescending(card => card) .Select(card => card.Value switch { Values.King => Output(card.Suit, 7), Values.Ace => $"It's an ace! {card.Suit}", Values.Jack => Output((Suits)card.Suit - 1 , 9), Values.Two => Output(card.Suit, 18), _ => card.ToString(), }); foreach(var output in processedCards) { Console.WriteLine(output); } } } Запишите результат выполнения программы. Мы не приводим решение — чтобы проверить свой ответ, вклю- чите код в консольное приложение и выполните его. Это серьезное испытание! Здесь происходит очень много всего — лямбда-выражения, выражение switch, методы LINQ, приведение типа перечислений, сцепленные вызовы методов и т. д . Не жалейте времени и разберитесь в том, как работает код, потом запишите свой ответ и только после этого запустите про- грамму. Если ваш ответ не совпал с результатом выполнения программы, у вас появляется отличная возможность разобраться, почему программа работает не так, как вы ожидали. Лямбда-выражение полу- чает два параметра, Suit и int, и возвращает ин- терполированную строку. Эти методы LINQ ничем не отлича- ются от тех, которые были пред- ставлены в начале главы. Метод Select исполь- зует команду switch для проверки номина- ла карты и генериру- ет соответствую- щую строку. Мы можем использовать OrderByDescending, пото- му что класс Card реализует IComparable<Card>. Возьми в руку карандаш
дальше 4 559 LINQи лямбда-выражения Этому лямбда-выражению переда- ется каждая пара элементов из двух объединяемых последовательностей. Это упражнение научит вас использовать модульные тесты для безопасного рефакто- ринга вашего кода Рефакторинг может быть хлопотным, даже раздражающим занятием. Вы берете работоспо- собный код и вносите в него изменения, чтобы улучшить его структуру, удобочитаемость и возможности повторного использования. Когда вы вносите изменения в свой код, очень легко допустить ошибку, из -за которой он работать перестанет, причем иногда внесенные ошибки оказываются на редкость коварными и трудноу- ловимыми. Здесь на помощь приходят модульные тесты. Одна из самых важных областей для применения модульных тестов разработчиками — повышение безопасности рефакторинга. Вот как это работает: • Прежде чем браться зарефакторинг, напишите тесты , которые подтверждают, что ваш код работает, — к а к тесты, добавленные ранее в этой главе для проверки класса ComicAnalyzer. • При рефакторинге класса просто запустите тесты для класса после внесения изменений. Это существенно сокращает цикл обратной связи для разработки — можно выполнить нормальную отладку, но этот способ ра- ботает намного быстрее, потому что код выполняется непосредственно в классе (вместо запуска программы и использования ее интерфейса для выполнения кода, использующего класс). • При рефакторинге метода можно начать даже с запуска конкретного теста или тестов, выполняющих этот метод.Если этот тест работает, можно перейти к полному набору тестов, который проверяет, что больше ничего не было сломано. • Если тест не проходит, не впадайте в уныние: на самом деле это хорошая новость! Вы узнали, что что-то сломалось, и теперь можете исправить ошибку. Используйте все, что вы узнали о лямбда-выражениях, командах switch и методах LINQ для рефакторинга класса ComicAnalyzer и метода Main. При помощи модульныхтестов убедитесь в том, что ваш код все еще работает так, как вы ожидали. Замените запросы LINQ в ComicAnalyzer ComicAnalyzer содержит два запроса LINQ: • Метод GroupComicsByPrice содержит запрос LINQ, использующий ключевое слово group для группировки комик- сов по цене. • Метод GetReviews содержит запрос LINQ, использующий ключевое слово join для объединения последователь- ности объектов Comic со словарем цен на выпуски. Измените запросы LINQ в этих методах, чтобы в них использовались методы LINQ OrderBy, GroupBy, Select и Join. Небольшая загвоздка: мы еще не рассказали вам о методе Join! Зато мы привели примеры, показывающие, как ис - пользовать IDE для исследования методовLINQ.Метод Join чуть сложнее других — но мы поможем вам разбить его на части. Он получает четыре параметра: sequence.Join(последовательность для объединения, лямбда-выражение для части 'on' объединения, лямбда-выражение для части 'equals' объединения, лямбда-выражение, которое получает два параметра и возвращает результат 'select'); Внимательно присмотритесь к частям «on» и «equals» запроса LINQ, соответствующим первым двум лямбда-выраже- ниям. Метод Join будет последним в цепочке. Подсказка: лямбда-выражение последнего параметра начинается так: (comic, review) => Когда оба модульныхтестабудут проходитьуспешно, рефакторинг классаComicAnalyzer можно считать завершенным. Замените команду switch в методе Main выражением switch Метод Main содержит командуswitch, которая вызывает приватные методы и присваивает их возвращаемые значения переменной done. Замените ее выражением switch с тремя ветвями. Чтобы протестировать выражение, запустите при- ложение — если при нажатии правильной клавиши выбудете получать правильный результат, значит, все готово . Упражнение Засценой
560 глава 9 полезные средства для создания последовательностей Используйте все, что вы узнали о лямбда-выражениях, командах switch и методах LINQ, для ре- факторинга класса ComicAnalyzer и метода Main. При помощи модульныхтестов убедитесь в том, что ваш код все еще работает так, как вы ожидали. Ниже приведены методы GroupComicsByPrice и GetReviews из класса ComicAnalyzer после рефакторинга: public static IEnumerable<IGrouping<PriceRange, Comic>> GroupComicsByPrice( IEnumerable<Comic> comics, IReadOnlyDictionary<int, decimal> prices) { var grouped = com ics .OrderBy(comic => prices[comic.Issue]) .GroupBy(comic => CalculatePriceRange(comic, prices)); return grouped; } public static IEnumerable<string> GetReviews( IEnumerable<Comic> comics, IEnumerable<Review> reviews) { var joined = com ics .OrderBy(comic => comic.Issue) .Jo in( reviews, comic => comic.Issue, review => review.Issue, (comic, review) => $"{review.Critic} rated #{comic.Issue} '{comic.Name}' {review. Score:0.00}"); return joined; } Ниже приведен метод Main с выражением switch, заменившим командуswitch, после рефакторинга: static void Main(string[] args) { var done = false; while (!done) { Console.WriteLine( "\nPress G to group comics by price, R to get reviews, any other key to quit\n"); done = Console.ReadKey(true).KeyChar.ToString().ToUpper() switch { "G" => GroupComicsByPrice(), "R" => GetReviews(), _ => true, }; } } Сравните два средних аргумента, передаваемых методу Join, с частями «on» и «equals» запроса join: on comic.Issue equals review.Issue. Запрос join начинается с «join reviews», так что первым аргументом, передаваемым методу Join, станет reviews. Последнее лямбда-выражение вызывается для каждой пары «ко- микс/обзор» с совпадением из двух объединенных последователь- ностей и возвращает строку, которая включается в результат. Сравните лямбда-выра- жения OrderBy и GroupBy с секциями orderby и group...by в запросе LINQ. Они почти идентичны. Выражение switch намного компактнее эквивалентной команды switch. Не все команды switch можно преобразовать в выражения switch — в данном случае это возможно, потому что во всех вариантах case присваивается значение одной и той же переменной (done). Упражнение Решение
дальше 4 561 LINQи лямбда-выражения Исследование класса Enumerable Мыуже знаем,что последовательности работают с циклами foreach иLINQ.Но какиеме- ханизмы обеспечиваютработу последовательностей? Начнем с класса Enumerable, а кон- кретно с его трех статических методов: Range, Empty и Repeat. Метод Enumerable.Range ужевстречался вамранее вэтой главе. IDE поможет нам понять,как работают два других метода. Введите Enumerable. и наведите указатель мыши на Range, Empty иRepeat в окне IntelliSense для просмотра объявлений икомментариев. Enumerable.Empty создает пустую последовательность любого типа Иногда требуетсяпередать пустуюпоследовательность методу, получающемуIEnumerable<T>(например, в модульном тесте). В таких случаях удобен метод Enumerable.Empty: var emptyInts = Enumerable.Empty<int>(); // пустая последовательность int var emptyComics = Enumerable.Empty<Comic>(); // пустая последовательность ссылок на Comic Enumerable.Repeat возвращает значение, повторенное заданное количество раз Предположим, вамнужна последовательностьиз 100значений цифры«3», или 12строк«yes», или 83иден- тичных анонимныхобъектов. Вынеповерите,насколькочасто такоепроисходит!Дляэтогоможновос- пользоватьсяметодомEnumerable.Repeat—онвозвращаетпоследовательностьповторяющихсязначений: var oneHundredThrees = Enumerable.Repeat(3, 100); var twelveYesStrings = Enumerable.Repeat("yes", 12); var eightyThreeObjects = Enumerable.Repeat( new { cost = 12.94M, sign = "ONE WAY", isTall = false }, 83); Как же работает IEnumerable<T>? Мыуже довольно давно пользуемсяIEnumerable<T>, но так инеответилина вопрос, что жепредставляетсобой последовательность споддержкойперебора.Самыйэф- фективныйспособпонять что-то —попытаться сделать это своими руками, поэтому в завершение этой главы мы построим несколько последовательностей с нуля. Еслибы вам пришлось п роек тиров ать ин терфейс I Enumer abl e<T > самос тоя тел ьн о, какие компоненты вы бы включили в него? Мозговой штурм
562 глава 9 что такое последовательность на самом деле Ручное создание последовательности с поддержкой перебора Допустим,у вас есть перечислениес видамиспорта: enum Sport { Football, Baseball, Basketball, Hockey, Boxing, Rugby, Fencing } Очевидно, можносоздать новыйсписокList<Sport> ивоспользоватьсяинициализаторомколлекциидля егозаполнения.Ночтобыразобратьсявтом, какработают последовательности, мысделаемэтовручную. Создайте новыйкласс сименемManualSportSequence, реализующийинтерфейсIEnumerable<Sport>.Он содержитдва компонента, возвращающиеIEnumerator: clas s Man ualSp ortSe quenc e : I Enume rable <Sport > { publi c IEn umera tor<S port> GetE numer ator() { r eturn new Manua lSpor tEnum erato r(); } Syste m.Col lecti ons.I Enume rator Syst em.Col lecti ons.I Enume rable .GetE numer ator( ) { r eturn GetE numer ator( ); } } Хорошо, так что же собой представляет IEnumerator? Это интерфейс, который позволяет перебратьсодержимое последовательности, перемещаясь от одного элемента кдругому.СвойствоCurrentвозвращает текущийэлемент впроцессе пере- бора. Метод MoveNext перемещается кследующему элементу последовательности, возвращаяfalse,если другихэлементовнеосталось.После вызоваMoveNextсвойство Current возвращает следующийэлемент. Наконец, метод Reset возвращается к на- чалупоследовательности.С этимиметодамиможнопостроить последовательность с поддержкойперебора. Реализуем IEnumerator<Sport>: usin g Sys tem.C ollec tions .Gene ric; clas s Man ualSp ortEn umera tor : IEnu merat or<Spo rt> { int current = -1; public Sport Current { get { return (Sport)current; } } public void Dispose() { return; } // О методе Dispose будет рассказано в главе 10. objec t Sys tem.C ollec tions .IEnu merat or.Cur rent { get { re turn Curre nt; } } public bool MoveNext() { v ar ma xEnum Value = En um.Ge tValu es(typ eof(S port) ).Len gth; if ((int)current >= maxEnumValue - 1) re turn false ; c urren t++; r eturn true ; } public void Reset() { current = 0; } } Иэтовсе,что необходимо длясоздания собственнойреализацииIEnumerable. Опробуйтееенапракти- ке. Создайтеновоеконсольноеприложение, добавьтеManualSportSequence иManualSportEnumerator, а затем переберитесодержимое последовательностив циклеforeach: var sports = new ManualSportSequence(); foreach (var sport in sports) Console.WriteLine(sport); Также необходимо реализо- вать интерфейс IDisposable, о котором вы узнаете в сле- дующей главе. Он содержит всего один метод Dispose. Наша ручная реализация IEnumerator преобразует тип int в перечисление. Статический метод Enum.GetValues использу- ется для получения обще- го количества элементов в перечислении, а int — для хранения индекса текущего значения. Когда мы выбрали команду меню Quick Actions «Implement interface», IDE использова- ла полные имена классов для IEnumerator и IEnumerable. IEn umer at or <T> Current MoveNext Reset Dispose
дальше 4 563 LINQи лямбда-выражения Использование yield return для создания собственной последовательности C#предоставляетнамного болееудобныйспособсозданияпоследовательностейсподдержкойперебора: команду yield return. Командаyield return— своего рода универсальныйавтоматическийсоздатель последовательностей.Лучшевсего понять еена примере.Воспользуемся многопроектнымрешением (азаодно потренируемсяв работе с ним). Добавьтеврешение новыйпроект консольного приложения — подобно тому,каквы добавляли про- ект MSTest ранеевэтойглаве,но на этотраз вместо типа проектаMSTestвыберитетотже тип проекта (ConsoleApp), которыйиспользовалсядлябольшинства проектоввкниге. Затемщелкнитеправойкноп- кой мыши на проекте под решением ивыберите команду «Set as startup project». Теперь при запуске отладчика вIDEбудетзапускаться новый проект. Также вы можете щелкнуть правойкнопкоймышина любомпроекте врешении ивыбратькомандуего запуска илиотладки. Код нового консольного приложения: static IEnumerable<string> SimpleEnumerable() { yield return "apples"; yield return "oranges"; yield return "bananas"; yield return "unicorns"; } static void Main(string[] args) { foreach (var s in SimpleEnumerable()) Console.WriteLine(s); } Запустите приложение.Оновыводитчетыре строки:apples, oranges,bananasи unicorns.Как этоработает? Исследование yield return в отладчике Установитеточку прерывания в первой строке метода Main и запустите отладчик. Затем используйте команду Step Into (F11/⇧⌘I),чтобы отладить кодстрокуза строкой: ≥ Выполняйтекодв пошаговомрежиме, покане достигнете первой строки метода SimpleEnumerable. ≥ Сделайтеследующийшаг.Происходит то же, чтопривыполнениикомандыreturn, — управление возвращается команде,изкоторойбылвызванметод: вданномслучаекомандеforeach,которая вызывает Console.WriteLine для вывода apples. ≥ Сделайтеещедвашага. Приложениевозвращаетсяметоду SimpleEnumerable, но первая команда метода пропускается иуправлениепередаетсяво вторую строку: ≥ Продолжайте пошаговое выполнение. Приложениевозвращается к циклу foreach, затем пере- ходитк третьей строке метода,снова к циклу foreach и,наконец, кчетвертойстрокеметода. Такимобразом, yield return заставляет метод вернуть последовательность с поддержкой перебора. Дляэтогопри каждом вызовевозвращаетсяследующийэлементпоследовательности,а такжеотслежи- ваетсяточка,изкоторойпроизошелвозврат,чтобы продолжить работус тогомомента,на которомона была прервана. Этот метод возвращает IEnumerable<string>, по- этому каждая команда yield return возвращает строко- вое значение. Каждый раз, когда цикл foreach получает элемент из последова- тельности, возвращаемой мето- дом SimpleEnumerable, он передает управление методу сразу же после выполнения последней команды yield return.
564 глава 9 yield return создает последовательности Использование yield return для рефакторинга ManualSportSequence Выможетесоздать собственнуюреализацию IEnumerable<T>, используяyield return для реализации метода GetEnumerator. Например, ниже приведен класс BetterSportSequence, который делает точно тожесамое,что иметодManualSportSequence. Эта версияполучается намного компактнее,потому что она используетyield return в своей реализации GetEnumerator: using System.Collections.Generic; class BetterSportSequence : IEnumerable<Sport> { public IEnumerator<Sport> GetEnumerator() { int maxEnumValue = Enum.GetValues(typeof(Sport)).Length - 1; for (int i = 0; i <= maxEnumValue; i++) { yield return (Sport)i; } } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } } Добавьтеврешениеновыйпроектконсольногоприложения.Добавьте новый классbetterSportSequence, измените метод Main для созданияего экземпляра иперебора последовательности. Добавление индексатора для BetterSportSequence Выуже видели, что yield return может использоваться в методе для создания IEnumerator<T>. Также с ее помощью можно создать класс, реализующий IEnumerable<T>. Одно из преимуществ создания отдельного класса для последовательности связано с возможностью добавления индексатора. Выуже использовали индексаторы: каждыйраз,когда вывключаетев программу квадратныескобки[]для из- влечения объекта из списка,массива илисловаря(например,myList[3] илиmyDictionary["Steve"]), вы используете индексатор.В действительностииндексатор является обычным методом. Он очень по- хож на свойство,если несчитать того,что он получаетодинименованный параметр. ВIDEсуществует особеннополезныйфрагменткода, помогающий вдобавлениииндексаторов. Введите indexer идва раза нажмите клавишутабуляции;IDE автоматически добавит заготовкуиндексатора впрограмму. Индексатор для класса SportCollection выглядит так: public Sport this[int index] { get => (Sport)index; } Вызовиндексатора[3] возвращает значение Hockey: var sequence = new BetterSportSequence(); Console.WriteLine(sequence[3]); Повнимательнееприсмотритесь кпроисходящемупри использовании фрагмента для создания индексато- ра — онпозволяет задать тип. Вы можетеопределить индексатор, которыйполучаетразные типы, включая строки и даже объекты. И хотя индексатор имеет только get-метод,онтакже можетвключать set-метод (вроде тех,которыеиспользуются для присваивания значенийэлементовList). Команда yield return может ис- пользоваться для реализации мето- да GetEnumerator в IEnumerable<T> и создания собственной последова- тельности с поддержкой перебора. Последовательности не являются коллекциями Попробуйте создать класс, реализующий ICollection<int>, и воспользуйтесь меню Quick Actionsдля реализации его ком- понентов. Вы увидите, что коллекция должна реализовать не только методы IEnumerable<T>, но и дополнительные свойства (включая Count) и методы (включаяAdd и Clear). Отсюда видно, что коллекция работает не так, как последо- вательность с поддержкой перебора. Будьте осторожны!
дальше 4 565 LINQи лямбда-выражения Создайте класс с поддержкой перебора, который в процессе перебора возвращает последо- вательность int, состоящую из степеней 2 (начиная с 0 и заканчивая наибольшей степенью 2, которая может быть сохранена в int). Используйте yield return для создания последовательности степеней 2 Создайте класс с именемPowersOfTwo, реализующийIEnumerable<int>. Он должен содержатьцикл for, который начинается с 0 и использует yield return для возвращения последовательности, содержащей все степени 2. Приложение должно выводить на консоль следующую последовательность: 1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384 32768 65536 131072 262144 524288 1048576 2097152 4194304 8388608 16777216 33554432 67108864 134217728 268435456 536870912 1073741824 Верните желаемую последовательность значений Выможете использоватьметоды статического класса System.Math для решения следующих задач: • Вычисление конкретной степени 2: Math.Pow(power, 2) • Определение наибольшей степени 2, которая может быть сохранена в int: Math.Round(Math.Log(int. MaxValue, 2)) В: Кажется, я понимаю, как работает командаyield return, но можете ли вы объяснить, как она передает управление в середину метода? О:Когда вы используетеyield returnдля создания последо- вательности с поддержкой перебора, происходит нечто такое, чего вы еще не видели в C#. Обычно когда метод выполняет команду return, ваша программа выполняет команду сразу же после той, в которой был вызван метод. То же самое происходит при пере- боре последовательности, созданнойyield return, — с одним отличием: в ходе перебора запоминается последняя командаyield return, выполненная в методе.Затем при перемещении к следующе- му элементу последовательности, вместо того чтобы начинать от начала метода, ваша программа выполняет следующую командуза последним выполненным вызовомyield return. Это позволяет вам построить метод, который возвращаетIEnumerable<T> простой серией команд yield return. В:Когда ядобавил класс, реализующийIEnumerable<T>, мне пришлось добавить метод MoveNext и свойство Current.При использовании yield return как я реализовал этот интерфейс без обязательной реализации этихдвух компонентов? О: Когда компилятор встречает метод с командойyield return, который возвращаетIEnumerable<T>, он автоматически добавляет метод MoveNext и свойствоCurrent.Привыполнении первая обна- руженная команда yield return возвращает первое значение циклу foreach. Когда цикл foreach продолжается (вызовом методаMoveNext), выполнение возобновляется с команды, следую- щей непосредственноза последнейвыполненной командойyield return. Его методMoveNext возвращаетfalse, когда текущаяпози- цияпереборавыходит за последнийэлемент в коллекции. Следить завсем этим на бумаге можетбыть непросто, зато все существенно упрощается, если вы загрузите программу в отладчике, вот почему мы началис пошагового выполнения простой последовательности, использующей yield return. часто Зада вае мые вопросы Упражнение
566 глава 9 пр ов ерьте свои з нан ия Создайте класс с поддержкой перебора, который в процессе перебора возвращает последова- тельность int, состоящую из степеней 2 (начиная с 0 и заканчивая наибольшей степенью 2, кото - рая может быть сохранена в int). class Power sOfTwo : IEnum erable <int> { pu blic IE numerat or<int > GetEn umerato r() { v ar max Power = Math.R ound(M ath.Log (int.Ma xValue , 2)); for (in t power = 0; power < maxPow er; po wer++) yield return (int) Math.Po w(2, po wer); } IE numerat or IEnu merabl e.GetEn umerato r() => GetEnu merator (); } class Progr am { st atic vo id Main (strin g[] arg s) { foreach (int i in ne w Power sOfTwo( )) Conso le.Writ e($" { i}"); } } ¢ Модульные тесты представляют собой автомати- зированные тесты, которые позволяют убедиться в том, что код работает так, как ожидалось, и прове- сти безопасный рефакторинг вашего кода. ¢ MSTest — фреймворк модульного тестирования, т. е. набор классов, предоставляющих инструменты для написания модульных тестов. Visual Studio со- держит инструменты для выполнения и просмотра результатов модульных тестов. ¢ Модульные тесты используют тестовые утвержде- ния (assertions) для проверки конкретных аспектов поведения. ¢ Ключевое словоinternal предоставляет классам одного проекта доступ к другому проекту в многопро- ектной сборке. ¢ Добавление модульных тестов для граничных слу- чаев и аномальных данных делает ваш код более устойчивым к ошибкам. ¢ Лямбда-оператор => определяет лямбда-выраже- ния, т . е . анонимные функции, определяемые в од- ной команде, которые выглядят так: (входные-пара- метры) => выражение; ¢ Если классреализует интерфейс,команда«Implement Interface» в менюQuickActions приказываетIDE доба- вить все недостающие компоненты интерфейса. ¢ Секции orderby и where в запросах LINQ можно переписать с использованием методовLINQ OrderBy и Where. ¢ Оператор => может использоваться для преобразо- вания поля в свойство с get-методом, выполняю- щим лямбда-выражение. ¢ Оператор ?:(условный или тернарный оператор) по- зволяет создать выражение, в котором реализуется логика if/else. ¢ Методы LINQ, которые получают параметр Func<T1, T2>, могут вызываться с лямбда-выражением, кото - рое получает параметрT1 и возвращает значение T2. ¢ Оператор => может использоваться для создания выражений switch — аналогов команд switch, кото - рыевозвращают значение. ¢ Класс Enumerable содержит статические методы Range, Empty и Repeat, упрощающие создание по- следовательностей с поддержкой перебора. ¢ Команды yield return создают методы, возвра- щающие последовательности с поддержкой перебора. ¢ Когда метод выполняет командуyield return, ко - манда возвращает следующее значение в последова- тельности. При следующем вызове метода выполне- ние возобновляется со следующей команды после последней выполненной командыyield return. Не забудьте включить директивы: using System; using System.Linq; using System.Collections; using System.Collections.Generic; Упражнение Решение КЛЮЧЕВЫЕ МОМЕНТЫ
LINQи лямбда-выражения GO FISH! Упражнение: Go Fish В следующем упражнении мы построим карточную игру Go Fish, в которой вы играете против компьютерных игроков. Модульное тестирование очень важно в этомпроекте, потомучтомыбудемпрактиковатьразработкучерез тестирование— методологию,при которой модульные тесты пишутся ранее того кода, который они тестируют. 2 OF SPADES 2 OF DIAMONDS 2 OF HEARTS 4 OF SPADES 7 OF DIAMONDS 7 OF CLUBS 8 OF SPADES 8 OF HEARTS JACK OF CLUBS YOUR HAND GAME PROGRESS BOOKS ASK FOR A CARD YOU HAVE A BOOK OF ACES CHARLIE HAS A BOOK OF FIVES YOU HAVE A BOOK OF SIXES SKYLER HAS A BOOK OF QUEENS Перейдите к репозиторию GitHubданной книги и загрузите PDF-файл проекта: https://github.com/head-first-csharp/fourth-edition YOU ASK IF ANYONE HAS A TEN CHARLIE HAS 1 TEN SKYLER HAS 0 TENS YOU MUST DRAW FROM THE STOCK CHARLIE HAS 9 CARDS SKYLER HAS 7 CARDS
Ладно, что нужно купить? Проволочная сетка... Текила... Мармелад... Бинты... Да, я все записываю. Чтение и запись файлов 10 Прибереги последний байт для меня Иногда настойчивость окупается. Пока что все ваши программы жили недолго. Они запускались, некоторое время работали и закрывались. Но этого недостаточно, когда имеешь дело с важной информацией. Вы должны уметь сохранять свою работу. В этой главе мы поговорим о том, как записать дан- ные в файл, а затем о том , как прочитать эту информацию. Вы познакоми- тесь с потоками данных, узнаете о сохранении объектов в файлах с исполь- зованием сериализации, а также освоите работу с шестнадцатеричными и двоичными данными и кодировку Юникод.
570 глава 10 ба й т ы , п р о читанные из ф а й л а О б ъ е к т S t r e a m О б ъ е к т S t r e a m П р о г р а м м а Для чтения и записи данных в .NET используются потоки данных Поток данных, или просто поток (stream), — механизм передачи/полу- чения данных отпрограммы в .NET Framework.Каждыйраз, когда про- грамма читает илизаписываетфайл, соединяетсяс другимкомпьютером посетииливыполняет какие-либо действия, связанные с отправкойили получениембайтов, вы используете потоки. В однихслучаях потокиис- пользуются напрямую, в других — косвенно. Даже когда вы используете классы, которыене предоставляютпрямого доступа к потокам,в их вну- треннейреализации почти всегда задействованы потоки. input = stream.Read(...); Переменная input содержит данные, прочитанные из потока б а й ты, за п и с ы в аемые вфайл stream.Write(...); На выходе данные, записываемые в поток Каждый раз, когда вам потребуется прочитать данные из файла или записать данные в файл, вы используете объект Stream. Допустим, у вас имеется простое приложение, которое должно читать данные из файла. В простейшем варианте можно воспользоваться объектом Stream. А если вашему приложению потребуется записать данные в файл, оно может воспользоваться другим объектом Stream. чтен ие и зап ись б айтов Вы использу- ете объект Stream... ...а поток работа- ет непосредствен- но с файлом. Вы можете ис- пользовать другой объект Stream, но процесс остается тем же. П р о г р а м м а
дальше 4 571 чтение и запись файлов Что можно сделать с потоком: Записывать данные в поток. Запись данных в поток осуществляется методом Write(). 1 Читать данные из потока. Метод Read() используется для чтения данных из файла, сети илииз памяти(ивообщеизлюбогохранилища,использующего поток).Вы можете читать данные изочень большихфайлов—на - столько огромных,что онине помещаются впамяти. 2 Изменять текущую позицию в потоке. Большинство потоков поддерживают метод Seek(), который ищет позицию в потоке для чтения или вставки данных в конкретном месте. Тем не менее не все классыStream поддерживаютSeek, что логично, таккакв некоторыхисточникахпотоковыхданныхвозврат впринципеневозможен. 3 Stream Clos e Read Se ek Write Потоки позволяют читать и записы- вать данные. Вы- бирайте тип по- тока, подходящий для тех данных, с которыми вы работаете. Объект FileStream по- з во ляет чита ть файлы иза- писывать вних д ан ные. Объект MemoryStream позволяет читать данные из памяти изаписывать их впамять. Объект NetworkStream выполняетчтение и запись данных на другие компьютеры илиустройства по сети . Объ ект GZipStream сж има ет да нные, чтобы онизанима- ли мень ше мес та и создавалименьше проблем с загруз- койи хранением. Некоторые из методов класса Stream. Класс Stream является абстракт- ным, поэтому вы не можете созда- вать его экземпляры. FileStream Close Rea d S eek Write MemoryStream Close Re ad Seek Write NetworkStream Close Rea d S eek Write GZipStream Close Rea d S eek Write Различные потоки для разных данных Каждыйпотокявляетсясубклассом абстрактного классаStream, исуществуетмножество субклассов Stream, предназначенных для различных операций.В данной главе мы сосредоточимся на чтении изаписи в обычныефайлы,но все, что вы узнаетео потоках в этой главе,также применимо к сжа- тым изашифрованнымфайлам идажек сетевым потокам,которые вообщенеиспользуютфайлы. Все субклассы добавляют свойства и методы, отно- сящиеся к функционально- сти конкретного класса.
572 глава 10 Объект FileStream читает и записывает байты в файл Если вашей программе потребуется записать несколько строк текста вфайл, должно про- из ойти сл едую щее: Объект FileStream присоединяется к файлу. 2 Потокизаписываютвфайлыбайтовые данные, поэтомустрока, котораядолжна быть записана,преобразуетсявмассивбайтов. 3 ВызываетсяметодWrite(), которомупередаетсямассивбайтов. 4 Потокзакрывается, чтобы другиепрограммы могли получить доступ к файлу. 5 Высоздаете объектFileStream и приказываете ему записать данные вфайл. 1 Eureka! 691171141011079733 читаем данные из файлов В любой программе, использующей потоки, должна присутствовать директива «using System.IO;» Присоединить объект FileStream можно только к одному файлу за раз. Не забывайте закрывать потоки, это очень важно. Файл останется заблокирован- ным, и другие программы не смогут работать с ним, пока вы не закроете свой поток. 6 9 117 1 1 4 1 0 11079733 Эта процедура называ- ется кодированием; мы поговорим о ней чуть позже... О б ъ е к т F il e S t r e a m О б ъ е к т F il e S t r e a m О б ъ е к т F il e S t r e a m О б ъ е к т F il e S t r e a m
дальше 4 573 чтение и запись файлов Запись текста в файл за три простых шага В C# существует удобный класс StreamWriter, который упрощает всю эту процедуру. Вам нужно только создать объектStreamWriter и присвоить ему имя. Он автоматически создаст объектFileStream и откроетфайл. После этого вам остается только воспользоваться методами Write() и WriteLine() для записи в файл любых данных на вашеусмотрение. Для открытия и создания файлов используйте конструктор класса StreamWriter. Имяфайла можнопередать конструкторуStreamWriter. Втаком случаефайлоткрывается автоматически. В классеStreamWriterсуществуеттакжеперегруженный конструктор,по- зволяющий задать режимдобавленияданных: со значением true текст добавляется вконец существующего файла, а со значением false существующий файл удаляется и создается новыйфайл с тем жеименем. var writer = new StreamWriter("toaster oven.txt", true); 1 Для записи в файл используйте методы Write() и WriteLine() Этиметоды работают так же, как их аналоги из класса Console:Write() записывает текст, а WriteLine() добавляетктексту символ новой строки. writer.WriteLine($"The {appliance} is set to {temp} degrees."); 2 Для освобождения файла используйте метод Close. Еслиоставитьпотокоткрытымисоединенным сфайлом, тофайл окажется заблокированным и ни одна программа не сможетработать с ним. Не за- бывайте всегда закрывать своифайлы! writer.Close(); 3 Д у х овк а в ы с т а вляе тся ... н а 2 0 0 гра д у с о в О б ъ е к т S t r ea m W r i t e r О б ъ е к т F il e S t r e a m О б ъ е к т S t r e am W r i t e r О б ъ е к т Fil e S t r e a m Класс StreamWriter автоматически создает объект FileStream и управляет им.
574 глава 10 using System.IO; class Program { static void Main(string[] args) { StreamWriter sw = new StreamWriter("secret_plan.txt"); sw.WriteLine("How I'll defeat Captain Amazing"); sw.WriteLine("Another genius secret plan by The Swindler"); sw.WriteLine("I'll unleash my army of clones upon the citizens of Objectville."); string location = "the mall"; for (int number = 1; number <= 5; number++) { sw.WriteLine("Clone #{0} attacks {1}", number, location); location = (location == "the mall") ? "downtown" : "the mall"; } sw.Close(); } } Эта стро- ка созда- ет объект StreamWriter и сообща- ет ему, где находится фай л. Попробуйте определить, что происходит с перемен- ной location и тернарным оператором ?:. Дьявольский план Пройдохи ЖителиОбъектвиля долгое времябоялисьПройдохи, глав- ногопротивникаКапитанаВеликолепного. Ивот онрешил воспользоваться объектомStreamWriter, чтобы воплотить в жизнь свой зловещийплан.Посмотрим на негопоближе.Соз- дайте проект консольного приложенияи добавьте этот код в метод Main, начинаяс объявленияusing, потомучтокласс StreamWriter находится впространстве именSystem.IO: запись данных Таккаквыневключилиполныйпуть вимяфайла, выходнойфайл находится воднойпапке сдвоичным— еслиприложение выполняется изVisual Studio, проверьтепапкуbin\Debug\netcoreapp3.1впап- к е р ешения. Результат,записанныйвфайл secret_plan.txt: Методы Write и WriteLine класса StreamWriter работают так же, как ана - логичные методы класса Console: Write записывает текст, а WriteLine записывает текст с символом новой строки. Оба клас- са поддерживают {фигурные скобки}: sw.WriteLine("Clone #{0} attacks {1}", number, location); Когда вы включаете{0} в текст, этот заполнитель заменяется первым пара- метром после строки; {1} заменяется вторым параметром, {2} — третьим, и т. д . How I'll defeat Captain Amazing Another genius secret plan by The Swindler I'll unleash my army of clones upon the citizens of Objectville. Clone #1 attacks the mall Clone #2 attacks downtown Clone #3 attacks the mall Clone #4 attacks downtown Clone #5 attacks the mall Результат: StreamWriter при- надлежит простран- ству имен System.IO. Очень важно вызвать Close при завершении ра- боты сStreamWriter — тем самым вы разрываете связь с файлом и освобождаетедругие ресурсы, с которыми работает экземпляр StreamWriter. Если не закрыть поток, то может оказаться, что часть текста не была записана(а возможно, весь текст!). Пройдоха — главный враг Капитана Великолепного; призрачный сверхзлодей, стремящийся к захвату Объектвиля. Если вы используе- те другую версию .N ET, то подкаталог внутри Debug может выглядеть иначе.
дальше 4 575 чтение и запись файлов Мы д оба вили д ополнит е льное упра жне ние. Сметодом Blobbo происходит что-то странное. На первыхдвухмагнитахпри- ведены два разных объявления?Мы определили Blobbo как перегружен- ный метод — существуют две разные версии этого метода, каждая со своим наборомпараметров(какиперегружен- ные методы, использовавшиеся в пре- дыдущих главах). Развлечения с магнитами Какая неприятность! Из магнитов на холодильнике был аккуратно выложен код класса Flobbo, но кто-то хлопнул дверью и магниты упали на пол. Сможете ли вы расставить их так, чтобы метод Main выводил при- веденныйниже результат? static void M ain(str ing[] args) { Flo bbo f = new F lobbo(" blue ye llow") ; Str eamWrit er sw = f.Sno bbo(); f.B lobbo(f .Blobb o(f.Blo bbo(sw) , sw), sw); } Предполагается, что в начале всех файлов с кодом располагается директива System.IO; blue yellow green purple red orange Результат class Flobbo { private string zap; public Flobbo(string zap) { t his. zap = zap; } } public bool Blobbo(StreamWriter sw) { public bool Blobbo (bool Already, StreamWriter sw) { sw.W rit eLin e(z ap) ; sw. Clo se( ); return false; sw. Wri teL ine( zap ); zap = "green purple"; retu rn fal se; if (Already) { } el se { } sw. Wri teLi ne( zap ); zap = "red orange"; return true; } } public StreamWriter Snobbo() { ret urn new S trea mWr iter ("m aca w.tx t") ; } Результат рабо- ты приложения, записываемый в файл с именем macaw.txt.
576 глава 10 Развлечения с магнитами. Решение Вам было предложено сконструировать класс Flobbo из магнитов,чтобыполучить нужныйрезультат. c lass Fl obbo { priv ate st ring za p; public Flobbo(string zap) { this.zap = zap; } } public bool Blobbo(StreamWriter sw) { public bool Blobbo (bool Already, StreamWriter sw) { sw .Wr iteL ine (zap ); s w.C lose (); re turn fa lse; sw .Wr ite Line (za p); zap = "green purple"; ret urn fal se; if (Already) { } e lse { } sw .Wr ite Line (za p); zap = "red orange"; ret urn tru e; } } public StreamWriter Snobbo() { retu rn new St ream Wri ter ("ma caw .txt "); static void Main(string[] args) { Flobbo f = new Flobbo("blue yellow"); StreamWriter sw = f.Snobbo(); f.Blobbo(f.Blobbo(f.Blobbo(sw), sw), sw); } После завершения работы не за- будьте закрыть файлы. Подумайте и попытайтесь ответить, поче - му метод вызыва- ется после записи всего текста. пр ов ерьте решен ие } Предполагается, что в начале всех файлов с кодом располагается директива System.IO; blue yellow green purple red orange Результат Определение перегру- женных методов В главе 8 вы узнали, что метод Random.Next является перегруженным, — он существует в трех версиях с разными наборами параме- тров. Метод Blobbo тоже перегружен — у него два объявления с разными параметрами: public bool Blobbo(StreamWriter sw) и public bool Blobbo(bool Already, StreamWriter sw) Два перегруженных метода Blobbo полностью независимы друг от друга. Они обладают раз- ным поведением (как и перегруженные версии Random.Next). Если добавить эти два метода в класс, IDE будет отображать их как пере- груженные методы (опять-таки по аналогии с Random.Next). Доба вьт е код в конс ольное приложение. Результат будет записан в файл macaw.txt в той же папке, где находится двоичный файл, — подкаталоге папки bin\Debug внутри папки прое кт а. Просто напомним: в этих упраж- нениях мы намеренно выбра- ли невразумительные имена переменных и методов, потому что с содержательными имена- ми головоломки оказались бы сл ишком прос т ым и! Не и споль- зуйте такие имена в своем коде, хо рошо?
дальше 4 577 чтение и запись файлов using System.IO; class Program { static void Main(string[] args) { var folder = Environment.GetFolderPath(Environment.SpecialFolder.Personal); var reader = new StreamReader($"{folder}{Path.DirectorySeparatorChar}secret_plan.txt"); var writer = new StreamWriter($"{folder}{Path.DirectorySeparatorChar}emailToCaptainA.txt"); writer.WriteLine("To: CaptainAmazing@objectville.net"); writer.WriteLine("From: Commissioner@objectville.net"); writer.WriteLine("Subject: Can you save the day... again?"); writer.WriteLine(); writer.WriteLine("We’ve discovered the Swindler’s terrible plan:"); while (!reader.EndOfStream) { var lineFromThePlan = reader.ReadLine(); writer.WriteLine($"The plan -> {lineFromThePlan}"); } writer.WriteLine(); writer.WriteLine("Can you help us?"); writer.Close(); reader.Close(); } } Использование StreamReader для чтения файла Прочитаем секретные планыПройдохи при помощи StreamReader. Этот класс имеет многообщего со StreamWriter, крометого,чтовместо записифайла вы создаете объект StreamReader и передаете емуимяфайла, из которогобудут читаться данные, в конструкторе. МетодReadLine возвращает строку, содер- жащую следующую строку текста изфайла. Можно написать цикл, который читает данные построкам,пока поле EndOfStreamне станет равнымtrue, т. е. покане кончатсяновые строки длячтения.Добавьтеконсольное приложение, использующее StreamReader для чтения одного файла, и StreamWriter для записидругогофайла: Этот цикл читает строку из StreamReader и записыва- ет ее в StreamWriter. Передайте имя файла, который вы собираетесь прочитать, конструктору StreamReader. Свойство EndOfStream со- держит true, если все данные в файле были прочитаны. Класс StreamReader чита- ет символы из потоков, но сам потоком не является. Когда вы передаете имя файла его конструктору, он создает поток за вас и закрывает его при вызо- ве метода Close. Также он содержит перегруженный конструктор, получающий ссылку на Stream. Каждый из объектов StreamReader и StreamWriter создал собственный поток. Вызов методов Close заставляет их закрыть эти потоки. To: CaptainAmazing@objectville.net From: Commissioner@objectville.net Subject: Can you save the day... again? We've discovered the Swindler's terrible plan: The plan -> How I'll defeat Captain Amazing The plan -> Another genius secret plan by The Swindler The plan -> I'll unleash my army of clones upon the citizens of Objectville. The plan -> Clone #1 attacks the mall The plan -> Clone #2 attacks downtown The plan -> Clone #3 attacks the mall The plan -> Clone #4 attacks downtown The plan -> Clone #5 attacks the mall Can you help us? Результат Возвращает путь к папке Documents пользователя в Windows или к домашнему каталогу пользователя в macOS. Убедитесь в том, что файл secret_plan.txt был скопирован в эту папку! За информацией о других папках, доступных для поиска, обращайтесь к описанию перечисления SpecialFolder.
578 глава 10 сцепленные потоки Stream Close R ead Seek Write CryptoStream Close R ead Seek Write Данные могут проходить через несколько потоков Употоков .NETестьодноважноепреимущество: они позволяют передать данные приемнику через несколько потоков. К числу многочисленных типов данных потоков.NET CoreпринадлежитклассCryptoStream. Он позволяет зашифровать данные перед тем, как проделать с ними все остальные операции. Таким образом, вместо записи простого текста в обычный текстовый файл: Пройдоха может объединить эти потоки в цепочку и передать текст через объектCryptoStream, прежде чем записывать вывод вFileStream: I’ll c r e a t e an ar m y o f C lones a n d * 3 y d 4 ÿ Ö n dfr56 d ì ¢ L 1 — При работе с классом FileStream данные в виде тек- ста записываются непосред- ственно в файл. Обычный текст записывает- ся в объект CryptoStream. Объект FileStream за- писывает зашифрованный текст в файл. Класс CryptoStream наследует от аб- страктного класса Stream, как и все прочие потоковые классы. Объект CryptoStream соединяется с объ ектом FileStream и передает ему поток зашифрован- ного текста. Потоки можно ОБЪЕДИНИТЬ В ЦЕПОЧКУ. Один поток записывает свои данные в другой, который, в свою очередь, записывает свои данные в третий поток... концом цепочки часто является сетевой ресурс или файловый поток. О б ъ е к т C ry p t o S t r e a m О б ъ е к т Fil e S t r e a m О б ъ е к т Fil e S t r e a m зашифрованный файл text file I’ll cr e a t e a n ar m y * 3 y d 4 ÿ Ö n dfr56 d ì ¢ L 1 —
дальше 4 579 чтение и запись файлов У бассейна Ваша задача — взять фрагменты кода из бассейнаи разместить их впустых стро- ках классов Pineapple, Pizza и Party. Любой фрагмент можно использо- вать несколько раз, использовать все фрагменты не обязательно. Ваша цель — добиться того, чтобы про- грамма записала файл с именем order. txtс пятьюстроками,приведенныминиже. Каждый фраг- мент можно использовать несколько раз! Stream reader writer StreamReader StreamWriter Ope n Close Far go Utah Idah o Dakota Pineapple HowMany HowMuch HowBig HowSmall int long string enum cla ss pu blic private this clas s static = >= <= != == ++ -- for while foreach var ReadLine WriteLine Pizza Party West East South North That's all folks! order.txt class Pineapple { cons t str ing d = "de livery .txt"; publ ic en um Fargo { North, South, East, West, Flamingo } publ ic sta tic voi d Main (string [] arg s) { var o = n ew St reamWr iter ("orde r.txt") ; v ar pz = new Pizza (new Stre amWrit er (d , true )); p z. Id aho (Fargo. Flamin go); for( int w=3;w>=0;w--){ var i = new Pizz a (n ew St reamWr iter (d, fa lse)); i.Ida ho((Fa rgo)w) ; Party p = n ew Pa rty (new Stream Reader (d)) ; p.How Much(o ); } o . Wri teLine ("Th at’s a ll folk s!"); o. Close (); } } class Pizza { priv ate StreamW riter write r; publ ic Piz za( S treamW riter writer ) { t his.wr iter = writer ; } publ ic voi d Idaho ( Pi neapple .Fa rgo f) { w riter. Writ eLine (f); writer. Close (); } } class Party { priv ate StreamR eader reade r; publ ic Par ty( S treamR eader reader ) { t his.re ader = reader ; } publ ic voi d HowMu ch( StreamW riter q) { q . Wr iteLine (rea der. ReadLi ne ( )); r eader. Clos e (); } } Код записывает эти строки в файл order.txt . Какой текст приложение запишет в файл delivery.txt? Дополнительный вопрос M I N I Возьми в рукукарандаш
580 глава 10 class Pineapple { co nst string d = "delivery.txt"; public enum Far go { North, South, East, West, Flamingo } public static void Main(string[] args) { var o=new StreamWriter ("order.txt"); varpz=new Pizza (new StreamWriter (d, true)); pz. Idaho (Fargo.Flamingo); for ( int w=3;w>=0;w--){ vari=new Pizza (new StreamWriter (d, false)); i.Idaho((Fargo)w); Party p = new Party (new StreamReader (d)); p.HowMuch(o); } o. WriteLine ("That’s all folks!"); o. Close (); } } class Pizza { private StreamWriter writer; public Pizza( StreamWriter writer ) { this.writer = writer; } public void Idaho( Pineapple .Fargo f) { writer. WriteLine (f); writer. Close (); } } class Party { private StreamReader re ad er; public Party( StreamReader reader ) { this.reader = reader; } public void HowMuch( StreamWriter q) { q. WriteLine (reader. ReadLine ()); reader. Close (); } } У бассейна. Решение Класс Pizza хранит объект StreamWriter в приватном поле, а его метод Idaho за- писывает в файл элементы перечисления Fargo; для это- го используются их методы ToString(), автоматиче- ски вызываемые методом WriteLine(). Точка входа про- граммы. Здесь соз да ет ся объ- ект StreamWriter, который переда- ется классу Party. Затем элементы перечисления Fargo в цикле передают- ся методу Pizza. Idaho для вывода. Это перечисление исполь- зуется для вывода резуль- тата. В главе 8 вы узна- ли, что метод ToString перечисления возвращает эквивалентную строку, так что вызов Fargo.North. ToString() вернет строку «North». В классе Party имеется поле StreamReader; метод HowMuch() чита- ет из StreamReader строку и записывает ее в StreamWriter. составное форматирование West East South North That's all folks! order.txt Какой текст приложение запишет в файл delivery.txt? North Ре зул ь тат, ко- торый записыва- ется приложением в файл order.txt . M I N I Возьми в руку карандаш Решение
дальше 4 581 чтение и запись файлов В:Объясните, для чего нужны {0} и{1} при вызове методов StreamWriter Write и WriteLine? О:Привыводе строк в файл часто оказывается,что вместе с тек- стом нужно вывести содержимое множества переменных. И тогда приходится писать громоздкие конструкции следующего вида: writer.WriteLine("My name is " + name + "andmyageis"+age); Объединять строки оператором + утомительно, к тому же возрас- тает риск ошибок. Проще воспользоваться составным формати- рованием, т . е. форматной строкой с заполнителями вида{0}, {1},{2} ит. д., а т акже п еременнымидля замены заполнителей: writer.WriteLine( "My name is {0} and my age is {1}", name, age); Возможно, у вас возникла мысль — все это очень похоже на стро- ковую интерполяцию? И вы абсолютно правы! Часто строковая интерполяция читается проще, в других случаях вариант с фор- матнойстрокойболеенагляден. Как и при строковой интерполяции, форматные строки поддерживают форматирование.Например, {1:0.00} означает, что второй аргумент долженформатироваться как число с двумя знаками в дробной части, а {3:c} приказыва- ет отформатировать четвертый аргумент как денежную сумму в местной валюте. Да, и еще — форматные строки также работают с Console.Write и Console.WriteLine! В: Что это за поле Path.DirectorySeparatorChar, которое использовалось в консольном приложении сStringReader? О: Мы писали этот код так, чтобы он работал и в Windows, и в macOS. Для этого пришлось воспользоваться некоторыми средствами .NET Core, которые помогают добиться этой цели. ВWindows в качестве разделителя путей используется обратная косая черта (C:\Windows), а в macOS — обычная(/Users). Path.DirectorySeparatorChar — поле ,доступное толькодля чтения, в котором хранится разделитель путидля текущей операционной системы: \дляWindows или/дляmacOS и Linux. Мы также использовали метод Environment.GetFolderPath, ко- торый возвращает путь к одной из специальных папок текущего пользователя, в данном случае к папкеDocuments в Windows или домашнему каталогу в macOS. В: Ачто тамговорилось про преобразование строки в массив байтов? Как это работает? О: Наверняка вы слышали, что файлы на диске представлены в виде битов и байтов. Другими словами, при записи файла на диск операционная система воспринимает его как наборбайтов. Объекты StreamReader и StreamWriter преобразуют эти байты в понятные вам символы, т. е. выполняют кодирование и декоди- рование. Помните, в главе 4 упоминалось, что переменная типа byte хранит значения от 0 до 255? Все файлы на жестком диске представляют собой длинные последовательности чисел из этого диапазона. Программа, читающая и записывающая эти файлы, должна интерпретировать байты как осмысленные данные. На- пример, при открытиифайла в приложении Блокнот каждый байт преобразуется в символ: например, E соответствует 69, а a — 97 (впрочем, все зависит от кодировки... но об этом чуть позже).Соот- ветственно, при сохранении введенного в Блокнотетекста символы преобразуются обратнов байты. Это преобразование необходимо и для записи строки в поток. В: ЕслияиспользуюStreamWriter для записифайла, зачем мне знать, что он создает за меня объект FileStream? О:Есливыограничиваетесь записьюичтениемстрок изтекстового файла,для этогодействительнодостаточнообъектовStreamReader и StreamWriter. Но как только вы займетесь более сложными за- дачами, вам придется работать с другими потоками. Записывать в файл числа, массивы , коллекции и объекты StreamWriter не умеет. Впрочем,эта тема будетподробнорассмотреначуть позже. В:Почему вы постоянно говорите, что потоки нужно закрывать после окончания работы? О:Сообщал ли вам когда-нибудь текстовыйредактор, что он не может открыть файл, потому что «файл используетсядругимпри- ложением»?Windows блокирует открытыефайлы и не позволяет открывать в других приложениях. Ваши программы не являются исключением — Windows блокирует файлы, открытые вашими приложениями. Если вы не вызовете метод Close, файл может остаться заблокированным вплоть до завершения программы. Классы Console и StreamWriter могут использовать составное форматирование, при котором заполнители заменяются значениями параметров, передаваемых при вызове Write или WriteLine. часто Задав аем ые вопросы
582 глава 10 Работа с файлами и каталогами с использованием статических классов File и Directory КлассFile,какикласс StreamWriter,незаметно дляразработчикасоздаетпотокидля работысфайлами.Методыэтого классадают возможность выполнять большинство операций без предварительного создания объекта FileStream. Класс Directory пред- назначендляработы с каталогами,содержащимимножество файлов. Что можно сделать со статическим классом File: Проверить, существует ли файл. Дляпроверки существования файла использу- ется метод File.Exists. Онвозвращаетtrue,если файл существует, или false в противном случае. 1 Читать из файла и записывать данные в файл. Метод File.OpenRead()читает данные из файла, а методы File.Create() и File.OpenWrite() записыва- ютданныевфайл. 2 Присоединять текст кфайлу. Метод File.AppendAllText() присоединяет текст к существующему файлу; если файл не найден на моментвыполнения метода,онавтоматически создается. 3 Что можно сделать со статическим классом Directory: Соз да т ь новую папку Метод Directory.CreateDirectory()создает каталоги. От вас потребуется лишьуказать путь;методсделает все остальное. 1 Получить список файлов в папке МетодDirectory.GetFiles()создаетмассив с инфор- мациейофайлахв каталоге. Укажитепуть к каталогу, метод сделаетвсеостальное. 2 Удалить каталог. Потребовалось удалить каталог? Вызовите метод Directory.Delete(). 3 Получать информацию о файле. Методы File.GetLastAccessTime() и File. GetLastWriteTime() возвращают время и дату последнего обращения к файлу и его последнего из менения . 4 FileInfo работает как File Если вы собираетесь часто и помногу работать с файлами, имеет смысл создать экземпляр класса FileInfo вме- сто использования статических мето- дов класса File. Класс FileInfo делает все то же, что и класс File, кроме того, что для ра- боты с файлами потребуется создать его экземпляр. Вы можете создать новый экземпляр FileInfo и обратить- ся к его методу Exists или методу OpenRead точно так же, как это де- лалось с File. Между двумя классами есть принци- пиальное различие: класс File быстрее работает при небольшом количе- стве операций с файлом, в то время как класс FileInfo лучше подходит для многочисленных операций. файлы и каталоги Класс File является статиче- ским, т. е . представляет со- бой простой набор методов для работы с файлами. Класс FileInfo поддерживает создание экземпля- ров; после того, как это будет сделано, вы получаете доступ к тому же набору методов, что и при работе с классом File.
дальше 4 583 чтение и запись файлов Для работыс файламии папками в .NET существуют два класса смногочисленны- ми статическими методами и интуитивно понятными именами. Класс File содер- жит методы для работы с файлами,а класс Directory дает возможность работать с каталогами. Напишите, как, с вашей точки зрения, работают представленные слева строкикода,а затем ответьте надва дополнительныхвопросав конце. Код Что делает if (!Directory.Exists(@"C:\SYP")) { Directory.CreateDirectory(@"C:\SYP"); } if (Directory.Exists(@"C:\SYP\Bonk")) { Directory.Delete(@"C:\SYP\Bonk"); } Directory.CreateDirectory(@"C:\SYP\Bonk"); Directory.SetCreationTime(@"C:\SYP\Bonk", new DateTime(1996, 09, 23)); string[] files = Directory.GetFiles(@"C:\SYP\", " *.log", SearchOption.AllDirectories); File.WriteAllText(@"C:\SYP\Bonk\weirdo.txt", @"This is the first line and this is the second line and this is the last line"); File.Encrypt(@"C:\SYP\Bonk\weirdo.txt"); File.Copy(@"C:\SYP\Bonk\weirdo.txt", @"C:\SYP\copy.txt"); DateTime myTime = Directory.GetCreationTime(@"C:\SYP\Bonk"); File .SetLastWriteTime(@ "C:\SYP\copy.txt", myTime); File.Delete(@"C:\SYP\Bonk\weirdo.txt"); Этот метод вы еще не видели — по- пробуйте догадаться, что он делает. Почему мы поставили @ перед каждой стро- кой, передаваемой в аргументах этих методов? Все имена файлов в этих примерах начинаются с C:\ для работы в Windows. Что произойдет при попытке выпол- нить этот код в macOS или Linux? Возьми в руку карандаш
584 глава 10 Код Что делает if (!Directory.Exists(@"C:\SYP")) { Directory.CreateDirectory(@"C:\SYP"); } Проверяет, существует ли папка C:\SYP, и если не существует — создает ее. if (Directory.Exists(@"C:\SYP\Bonk")) { Directory.Delete(@"C:\SYP\Bonk"); } Проверяет, существует ли папка C:\SYP\ Bonk, и если существует — удаляет ее. Directory.CreateDirectory(@"C:\SYP\Bonk"); Создает каталог C:\SYP\Bonk. Directory.SetCreationTime(@"C:\SYP\Bonk", new DateTime(1996, 09, 23)); Устанавливает время создания для папки C:\SYP\Bonk на 23 сентября 1996 г. stri ng[ ] fi les = Dire cto ry. GetF ile s(@ "C:\ SYP \", "* .log", SearchOption.AllDirectories); Получает список всех файлов из C:\SYP, соответствующих шаблону *.log, включая все подходящие файлы в любом подката- логе. File.WriteAllText(@"C:\SYP\Bonk\weirdo.txt", @"This is the first line and this is the second line and this is the last line"); Создает файл с именем «weirdo.txt» (если он не существует) в папке C:\SYP\Bonk и записывает в него три строки текста. File.Encrypt(@"C:\SYP\Bonk\weirdo.txt"); Использует встроенные средства шиф- рования Windows для шифрования файла «weirdo.txt» с использованием реквизитов текущей учетной записи. File.Copy(@"C:\SYP\Bonk\weirdo.txt", @"C:\SYP\copy.txt"); Копирует файл C:\SYP\Bonk\weirdo.txt в C:\SYP\Copy.txt . DateTime myTime = Directory.GetCr eationTime(@"C:\SYP \Bonk"); Объявляет переменную myTime и присваи- вает ей время создания папки C:\SYP\Bonk. File. SetLastWri teTime(@"C :\SYP\copy .txt", myT ime); Из м ен яе т вр е м я п ос ле д не й зап и си в фай л copy.txt из C:\SYP\, чтобы оно было рав- но времени, хранящемуся в переменной myTi me . File.Delete(@"C:\SYP\Bonk\weirdo.txt"); Удаляет файл C:\SYP\Bonk\weirdo.txt . Для работыс файламиипапками в .NET существуют два класса с мно- гочисленными статическими методами и интуитивно понятными именами. Класс File содержит методы для работыс файлами, а класс Directory дает возможность работать скаталогами.Вам было предло- жено записать,чтоделает каждыйблоккода. Альтернатива для CryptoStream метод dispose Почему мы поставили @ перед каждой стро- кой, передаваемой в аргументах этих методов? Все имена файлов в этих примерах начинаются с C:\ для работы в Windows. Что произойдет при попытке выпол- нить этот код в macOS или Linux? @ не позволяет интерпретировать символы \ в строке как часть служебных последовательностей. Программа создастфайл с именем, начинающимся с «C:\», в одной папке с двоичным файлом. Возьми в руку карандаш Решение
дальше 4 585 чтение и запись файлов Интерфейс IDisposable обеспечивает корректное закрытие объектов Многие классы .NET реализуют исключительно полезный интерфейс IDisposable. Этот ин- терфейс содержит всего один метод Dispose. Если классреализуетIDisposable,он тем самым сообщает, что есть важныеоперации, которыетребуетсясделать длякорректногозавершения работы, — обычно из-за того, что выделенные ресурсы не освобождаются самостоятельно. Метод Dispose сообщает объекту, как освободить эти ресурсы. Исследование IDisposable в IDE Воспользуемсяфункцией IDE Go To Definition (или «Go to Declaration» на Mac) для просмотра определенияинтерфейсаIDisposable. Введите IDisposable впроизвольномместе внутрикласса, щелкните на этойстрокеправойкнопкоймышии выберитев меню команду GoToDefinition. Откроется новая вкладка с кодом. Вотчто выувидите: namesp ace System { /// <summ ary> /// Предо ставляет механиз м освобождения н еуправляемых рес урсов. /// </sum mary> public in terface IDisposa ble { /// <s ummary> /// Выполняет операции, определяемые приложением, при /// освобождении или с бросе неуправляе мых ресурсов. /// </ summary> void Dispose(); } } Многие классы выделяют такие важные ре- сурсы, как память, файлы и другие объекты. Это означает, что класс захватил данный ресурс и не вернет его системе, пока не по- лучит сигнал, что работа закончена. вы-де-лять, глагол. Раздавать ресурсы или обязанности для опреде- ленных целей. Любой класс, реализующий ин- терфейс IDisposable, должен немедленно освободить любые за- действованные ресурсы при вызове его метода Dispose. Это почти всегда становится последним действием перед завершением работы с объектом.
586 глава 10 Предотвращение ошибок файловой системы командами using Напротяженииглавывамтвердилионеобходимостизакрывать потоки. Ведь самые распространенные ошибки, с которыми сталкиваются программисты приработе с файлами, возникают какразиз-за того,что потокинебылизакрытыкакположено. Ксчастью, в C# имеется замечательный инструмент, позволяющий избежать подоб- нойситуации, — это интерфейсIDisposable с его методом Dispose. Если заключить кодработыс потоком в командуusing, потокначнет закрыватьсяавтоматически.Вам нужно только объявить ссылку на поток в команде using, а затем указать блок кода (вфигурныхскобках), вкотором используется эта ссылка. ВрезультатеC# автома- тически вызывает метод Dispose сразу жепослезавершенияблока. За командой using всегда следует объявление объекта... using (var sw = new StreamWriter("secret_plan.txt")) { sw.WriteLine("How I'll defeat Captain Amazing"); sw.WriteLine("Another genius secret plan"); sw.WriteLine("by The Swindler"); } ... а затем блок кода в фигурных скобках. После выполнения последней команды в блоке для использу- емого объекта вызы- вается метод Dispose. В данном случае на исполь- зуемый объект указывает ссылка sw, объявленная внутри команды using. Поэтому будет выполнен метод Dispose() класса Stream... который и закроет поток. В данном слу- чае речь идет вовсе не о тех командах (директивах) using, кото- рые распола- гаются в на- чале части код а. команды using могут предотвратить исключения По одной команде using на объект Операторы using можноразмещать друг за другом, при этом непо- требуются дополнительный наборфигурных скобок или дополни- тельные отступы: using (var reader = new StreamReader("secret_plan.txt")) using (var writer = new StreamWriter("email.txt")) { // команды, использующие StreamReader и StreamWriter } Если вы объявили объект в блоке using, метод Dispose будет вызван для этого объекта автоматически. Команда using объявляет п ерем енную sw, со дер жащ ую ссылку на новый объект StreamWriter, после которой идет блок кода. После того как будут выполнены все команды в блоке, блок using автоматически вызовет sw.D isp ose. Каждый поток содержит метод Dispose, который закрывает этот поток. Поток, объявленный в команде using, будет закрыт гаран- тированно! И это важно, потому что некоторые потоки записывают последнюю часть своих данных только в момент закрытия.
дальше 4 587 чтение и запись файлов Потоки MemoryStream и хранение данных в памяти Донастоящего момента мы использовали потоки для чтенияи записифайлов. А есливы хотите прочитать данныеиз файла,а потом... как-то ихобработать?Используйте классMemoryStream, который отслеживает все переданные ему данные и сохраняет их в памяти. Например, можно создать новыйобъектMemoryStreamипередать его варгументе конструктораStreamWriter, после чего любые данные, записываемые с помощью StreamWriter,будут отправлятьсяMemoryStream. Дляполученияэтих данных можно воспользоватьсяметодом MemoryStream.ToArray, который возвращаетвседанные,переданные MemoryStream,в видебайтового массива. Преобразование байтовых массивов в строки методом Encoding.UTF8.GetString Одна из самых распространенных операций, выполняемых с байтовыми массивами, — преоб- разованиеихвстроки. Например,если у вас имеетсябайтовый массив с именем bytes, один из способовего преобразованиявстроку выглядиттак: var converted = Encoding.UTF8.GetString(bytes); Нижеприведено маленькое консольное приложение, которое использует составноеформати- рование для записи числа вMemoryStream, преобразует его вбайтовый массив,а затем встроку. Есть толькоодна проблема... оно неработает! Создайтеновоеконсольноеприложениеивключите внего приведенныйниже код. Вамудастся обнаружить ошибку и исправить ее? using System; using System.IO; using System.Text; class Program { static void Main(string[] args) { using (var ms = new MemoryStream()) using (var sw = new StreamWriter(ms)) { sw.WriteLine("The value is {0:0.00}", 123.45678); Console.WriteLine(Encoding.UTF8 .GetString(ms.ToArray())); } } } В:Напомните, почему мы ставим@ в начало строк с именами файлов в упражнении «Возьми в руку карандаш»? О: Потому, что в противном случае символы \S в «C:\SYP» будут интерпретированы какнедопустимая служебная последовательность, и программа выдастисключение. Когда вывключаете в свою программу строковый литерал, компилятор преобразует служебные последова- тельности (такие, как \n и\r) в специальные символы. Именафайлов Windows могут содержать символы \, но в строках C# этот символ обычно начинает служебную последовательность. Если поставить @перед строкой, тем самым вы прикажетеC# не интерпретировать служебные последовательности. Крометого, @ перед строкой прика- зывает C# включать разрывы строк в текст, так что если вы нажмете Enter в процессе ввода, этот разрыв строкибудет включен в вывод. В:Для чегонужны служебные последовательности? О: Служебная последовательность позволяет включать специ- альныесимволы в строки. Например,\n представляет символ новой строки, \t— табуляцию, а \r — возврат курсора(или только его по- ловину вWindows — в текстовых файлах Windows строкадолжна завершаться комбинацией\r\n;в macOS иLinux строки завершаются \n).Есливампонадобится включить кавычки встроку, используйте последовательность \" — она не будет интерпретирована как за- вершение строки. Если же вы захотите включить символ\ в строку, но так, чтобыC# не интерпретировал его как начало служебнойпо- следовательности, используйтеудвоенную обратнуюкосую черту:\\. Метод MemoryStream.ToArray возвращает все потоковые данные в виде бай- тового массива. Метод GetString преобразует байтовый массив в строку. Это приложение неработа- ет! Оно должно выводить строку текста на консоль, но оно вообщеничего невы- водит. Мы объясним, что не так, но сначала попробуйте обнаружить ошибку с амосто- ятел ьн о. Подсказка: вы сможете определить, когда потоки за- к ры ваю тся? Сделайте это!
588 глава 10 в какой момент потоки сбрасываются на диск Шерлок Холмс однажды сказал: «Факты!Факты!Факты!Когда под рукой нет глины, из чего лепить кирпичи?» Начнем с места преступления: кода, который не работает. Мы извлечем из него всю возможную информацию и найдем улики. А сколько из этих улик нашли вы? ‘ Мы создаем экземпляр StreamWriter, который передает данные новому экземпляруMemoryStream. ‘ StreamWriter записывает строку текста в MemoryStream. ‘ Содержимое MemoryStream копируется в массив и преобразуется в строку. ‘ Все это происходит в блоке using, так что потоки определенно закрываются. Если вы обнаружили все улики, поздравляем — ваши навыки расследования в коде, бесспорно, совершенству- ются! Но как и в каждой настоящей тайне, всегда остается последняя улика — какой-то факт, который выузнали ранее и который играет ключевую роль в раскрытии преступления и поиске настоящего виновника. Мы использовали блок using, поэтому мы точно знаем, что потоки закрываются. Но когда они закрываются? И тут обнаруживается ключ ко всей тайне, первостепенная улика, о которой мы узнали еще до преступления: не- которые потоки не записывают все свои данные, пока не будут закрыты. Объекты StreamWriter и MemoryStream объявляются в одном блоке using, поэтому оба метода Dispose вы- зываются после выполнения последней строки блока. Что это значит? То, что метод MemoryStream.ToArray вызывается до закрытия StreamWriter. Проблему можно решитьдобавлением вложенногоблока using, чтобы сначала объект StreamWriter закрывался, а потом вызывался метод ToArray: using System; using System.IO; using System.Text; class Program { static void Main(string[] args) { using (var ms = new MemoryStream()) { using (var sw = new StreamWriter(ms)) { sw.WriteLine("The value is {0:0.00}", 123.45678); } Console.WriteLine(Encoding.UTF8.GetString(ms.ToArray())); } } } По следу Внутренний блок using гаранти- рует, что объект StreamWriter будет закрыт после вызова мето- да MemoryStream.ToArray. Объект MemoryStream объявляет- ся во внешнем блоке using, так что он может оставаться открытым даже после закрытия StreamWriter. Объекты потоков часто хранят в памяти буферизованные данные, ожидающие записи надиск. Когда поток записывает все оставшиеся данные, это называется сбросом надиск. Чтобы сбросить надиск все буферизованныеданные без закрытия потока, вызовите метод Flush. Мы дали вам возможность обна- ружить ошибку самостоятельно. А теперь посмотрите, как испра- вили ее мы.
дальше 4 589 чтение и запись файлов В главе 8 мы создали класс Deckдляхранения последовательности объектовCard, с методами для сброса колоды до 52 карт по порядку, тасования карт для размещения их в случайном по- рядке, а также сортировки карт для возвращения их к исходному порядку. Теперь мыдобавим метод для записи карт в файл, а также конструктор, позволяющий инициализировать новую колодукартами, прочитанными из файла. Просмотрите классы Deck иCard, написанные в главе 8 Мы создали класс Deck посредством расширения обобщенной коллекции объектов Card. Это позволило нам использовать некоторые полезные компоненты, унаследованные Deck от Collection<Card>. ‘ Свойство Count возвращает количество карт в колоде. ‘ МетодAdd добавляет карту на верх колоды. ‘ Метод RemoveAtудаляет карту с заданным индексом из колоды. ‘ Метод Clear удаляет все карты из колоды. Эта реализация станет хорошей отправной точкой для добавления методаReset, который очищает колоду, а потом добавляет52 карты по порядку(от тузадо короля в каждой масти). Метод Deal удаляет карту с верха колоды и возвращает ее, метод Shuffle переставляет карты в случайном порядке, а метод Sort снова выстраивает их по порядку. Добавьте метод для записи всех карт из колоды в файл КлассCard содержит свойствоName, которое возвращает строку вида «Three ofClubs» или «Ace of Hearts». Добавьте метод с именем WriteCards, который получает строку с именем файла в параметре и записывает имя каждой карты в файл, таким образом, если сбросить колоду, а потом вызвать WriteCards, в файл будут записаны52 строки, по одной длякаждой карты. Добавьте перегруженный конструкторDeck, который читаетданные колоды ка рт из фа йла Добавьте второй конструктор в классDeck.Ниже приведено описание того, что он долженделать: pub lic Deck (str ing file name) { // Создать новый объект StreamReader для чтения файла. // Для каждой строки в файле выполнить следующие четыре операции: // Использовать метод String.Split: var cardParts = nextCard.Split(new char[] { ' ' }); // Использовать выражение switch для получения масти каждой карты: var suit = cardParts[2] switch { // Использовать выражение switch для получения номинала каждой карты: var value = cardParts[0] switch { // Добавить карту в колоду. } В главе 9 вы узнали, что выражение switch долж- но быть исчерпывающим, поэтому добавьте вари- ант по умолчанию, который выдает исключение InvalidDataException при обнаружении неопознан- ной масти или номинала, — т е м сам ым будет гаран- тирована правильность всех карт. Следующий метод Main может использоватьсядля те- стирования приложения. Он создает колоду из 10 слу- чайных карт, записывает ее в файл, а затем читает этот файл во вторую колоду и выводит описание каж- дой карты на консоль. Collection<Card> Count Add RemoveAt Clear Deck(constructor) Reset Deal Shuffle S ort Deck(filename) WriteCards Deck Мы добавим ме- тод WriteCards и перегруженный конструктор. static void Main(string[] args) { var filename = "deckofcards.txt"; Deck deck = new Deck(); deck.Shuffle(); for (int i = deck.Count - 1; i > 10; i--) deck.RemoveAt(i); deck.WriteCards(filename); Deck cardsToRead = new Deck(filename); foreach (var card in cardsToRead) Console.WriteLine(card.Name); } Метод String.Split позволяет задать массив символов- разделителей (в данном случае пробел), использует их для разбиения строки на части и возвращает массив, сод ерж ащи й все части . Упражнение
590 глава 10 разные операционные системы и разные завершители строк Ниже приведены два метода, добавленные в класс Deck. Метод WriteCards использу- ет StreamWriter для записи карт в файл, а перегруженный конструктор Deck исполь- зует StreamReader для чтения карт из файла. Так как вы работаете с StreamWriter и StreamReader, не забудьте добавить using System.IO; в начало файла. public void WriteCards(string filename) { using (var writer = new StreamWriter(filename)) { for(inti=0;i<Count;i++) { writer.WriteLine(this[i].Name); } } } public Deck(string filename) { using (var reader = new StreamReader(filename)) { while (!reader.EndOfStream) { var nextCard = reader.ReadLine(); var cardParts = nextCard.Split(new char[] { ' ' }); var value = cardParts[0] switch { "Ace" => Values.Ace, "Two" => Values.Two, "Three" => Values.Three, "Four" => Values.Four, "Five" => Values.Five, "Six" => Values.Six, "Seven" => Values.Seven, "Eight" => Values.Eight, "Nine" => Values.Nine, "Ten" => Values.Ten, "Jack" => Values.Jack, "Queen" => Values.Queen, "King" => Values.King, _ => throw new InvalidDataException($"Unrecognized card value: {cardParts[0]}") }; var suit = cardParts[2] switch { "Spades" => Suits.Spades, "Clubs" => Suits.Clubs, "Hearts" => Suits.Hearts, "Diamonds" => Suits.Diamonds, _ => throw new InvalidDataException($"Unrecognized card suit: {cardParts[2]}"), }; Add(new Card(value, suit)); } } } Выражение switch проверя- ет, совпадает ли первое слово в строке с заданными значе- ниями. Если совпадение будет найдено, переменной «value» присваивается соответству- ющий элемент из перечисле- ния Value. То же самое делается с третьим словом в строке, которое пре- образуется в элемент перечисления Suit. Эта строка приказыва- ет C# разбить строку nextCard по пробелам. Таким образом, строка «Six of Diamonds» будет преобразована в массив {"Six", "of", "Diamonds"} Секция по умолчанию выражения switch выдает исключение, если файл содержит недействительную карту. Упражнение Реше н ие
дальше 4 591 чтение и запись файлов ¢ Каждый раз, когда программа читает данные из фай- ла или записывает данные в файл, вы используете объект Stream. Класс Stream является абстрактным и имеет ряд специализированных субклассов. ¢ Класс FileStream предназначен для чтения и записи данных в файлы. Класс MemoryStream читает и за- писывает данные в память. ¢ Для записи данных в поток используется метод Write, а для чтенияданных — метод Read. ¢ КлассStreamWriter предоставляет простые средства длязаписи данных вфайл. StreamWriter автоматиче- ски создает объект FileStream и управляет им. ¢ Класс StreamReader читает символы из потока, но сам потоком не является. Он создает поток за вас, читает из него данные и закрывает его при вызове метода Close. ¢ Методы Write и WriteLine классов StreamWriter и Console поддерживают составное форматирова- ние: они получают форматную строку с заполнителя- ми вида {0},{1},{2} и т. д. , с поддержкой расширенно- го форматирования вида {1:0.00} и{3:c}. ¢ Path.DirectorySeparatorChar — поле , доступное только для чтения, в котором хранится раздели- тель пути для текущей операционной системы: \ для Windows или /для macOS и Linux. ¢ Метод Environment.GetFolderPath возвращает путь коднойиз специальных папокдлятекущегопользователя (например, к папке Documents пользователя вWindows или домашнему каталогу пользователя в macOS). ¢ Класс File содержит статические методы, включая Exists (проверяет, существует ли файл), OpenRead и OpenWrite (получают потоки для чтения и записи в файл), а также AppendAllText (записывает текст в файл одной командой). ¢ КлассDirectory содержит статические методы, вклю- чая CreateDirectory(создает папки), GetFiles (получа- ет список файлов) и Delete (удаляет папки). ¢ Класс FileInfo похож на класс File, но вместо вызо- ва статических методов создается экземпляр этого кл ас са. ¢ Не забывайте всегда закрывать поток после завер- шения работы с ним. Некоторые потоки записывают остаток своих данных на диск только при закрытии или при вызове метода Flush. ¢ Интерфейс IDisposable обеспечивает гарантиро- ванное закрытие объектов. Он содержит один метод Dispose, который предоставляет механизм освобож- дения неуправляемых ресурсов. ¢ Используйте команду using для создания экзем- пляра класса, реализующего IDisposable. За коман- дой using следует блок кода; объект, созданный в команде using, уничтожается в концеблока. ¢ Используйте несколько команд using подряд для объявления объектов, которые должны бытьуничто- жены в конце одного блока. В Windows и macOS используются разные завершители строк Если выработаетев Windows, откройте Блокнот. Если вы работаетев macOS, откройте TextEdit. Создайте файл из двух строк: первая строка содержит символы L1, а вторая — символы L2. В Windows файл содержит следующие шесть байтов: 76 49 13 10 76 50. В macOS файл содержит следующие пять байтов: 76 49 10 76 50. Видитеразличия? Первая и вторая строки кодируются одними и теми же байтами: L — 76, 1 — 49, а 2 — 50. Разрыв строки кодируется иначе: в Windows он кодируется двумя байтами 13 и 10, а в macOS — одним байтом 10. В этом проявляютсяразличия между завершителями строк в стиле Windows и в стиле Unix (macOS являетсяразновидностью Unix). Если вам нужно написать код, который работает вразных операционных системах и записываетфайлы с завершителями строк, вы можете воспользоваться статическим свойством Environment.NewLine, котороевоз- вращает "\r\n" в Windows или "\r" в macOS или Linux. КЛЮЧЕВЫЕ МОМЕНТЫ
592 глава 10 Столькокодадля чтения всего однойкарты? Немноговато ли? А что делать собъектами смножеством значений и полей?Неужелимне придется писать оператор switch для каждого? Существует более простой способ сохранения объектов в файлах — сериализация (serialization). Сериализацией называется запись полного состояния объекта вфайл или строку.Обратныйпроцесс — чтение состояния объекта изфайла или строки — называется десериализацией.Таким образом, вместо скрупулезной записи в файл каждого поля и значения можно сохра- нить объект посредством сериализации его в поток. Сериализация приводит к деструктуризации объекта, т. е.превращению его внаборбайтов. С другойстороны, придесериализацииобъекта данныечитаютсяизфайла ииспользуются длясоздания объекта. сериализация позволяет сохранять целые объекты Ладно, чтобы не было недоразумений: существует ме- тод Enum.Parse, который преобразует строку «Spades» в элемент перечисления Suits.Spades. У него даже есть парный метод Enum.TryParse, работающий как метод int.TryParse, который встречался вам в книге. Впрочем, в рассматриваемом случае лучше использовать сериали- зацию, в чем вы скоро убедитесь...
дальше 4 593 чтение и запись файлов Созданный вами экземпляр объ- екта обладает состоянием. При этом объект«знает» только то, чем экземпляр одного класса отлича- ется от другого экземпляра этого жекласса. Что происходит с объектом при сериализации? 1 Объект в куче 2 Се риа лиз ова нный объ ект 0 0 1 0 0 1 0 1 ширина 0 1 0 0 0 1 1 0 высота 00 100 101 010 00 110 При сериализации объекта в C# полностью сохраняется состоя- ниеобъекта, что позволяет воссоз- дать идентичный экземпляр(объ- ект)вкуче. Экземпляры пере- менных width и height были сохра- нены в файле file.dat вместе с дополни- тельной информа- цией, необходимой для дальнейшего восстановления объекта (напри- мер, информацией о типе самого объ- екта и каждого из его полей). file.dat Эт от объ- ект имеет два байтовых поля, width и height. Трансформацияобъектаприкопированииегоиз кучиисохранениивфайле выглядиттаинственно,но на самом делетам все очень просто. 3 А потом... Позднее—может быть,черезмного дней и в другой программе — вы можете вер- нуться к файлу и провести десериали- зацию объекта. Исходный класс будет восстановлен из файла в точно таком состоянии, вкаком онбылсериализован, со всеми полямии значениями. Объект снова в куче
594 глава 10 Но что именно СЧИТАЕТСЯ состоянием объекта? Что необходимо сохранить? Выужезнаете,чтообъект хранит информацию вполях исвойствах. Следова- тельно,присериализациинужно сохранить вфайле каждое изэтихзначений. Сболеесложнымиобъектами сериализация перестаетбыть тривиальным делом. Переменныеразных типов — сhar, int, double идр. — можно записать вфайлбез изменений.А какбыть,если всоставе объекта присутствует пере- менная-экземпляр, содержащаяссылкуна объект?Аеслипять переменных-эк - земпляров, содержащихссылкина объект?А еслиэтиссылки, всвоюочередь, указывают на другие ссылки? Подумайтеоб этом. Какаячасть объекта являетсяпотенциальноуникальной? Чтоименнонеобходимо восстановить, чтобыполучить объект, идентичный со- храненному?Такили иначе, нов файл нужнозаписать все,чтохранитсяв куче. Как им образом следу ет с охран ить объ ек т Car , чтобы п отом его можно было восстановить в исходном состоянии? Предп оложим , в автом обиле ед ут три п ас сажира, он имеет трехлитровый двигатель и всесезонные шины... разве вся эта информация не является частью состояния объекта Car? И что с нейделать? Объект Car содер- жит ссылку на объект Engine (Двигатель), массив объектов Tire (Шина) и перечисле- ние List<> с объектами Passenger (Пассажир). Это составные части его состояния. Что должно произойти с ними при сериализа- ции ? Каждый из объ- ектов Passenger содержит ссылки на другие объек- ты. Следует ли их сохранять? Объект Engine является при- ватным. Нужно ли его сохранять наряду с другими? связанные объекты образуют граф О б ъ е к т E n g i n e О б ъ е к т м а с с и в а T i r e [ ] О б ъ е к т L is t < P a s s e n g e r s > О б ъ ект C a r «Напряги мозги» — версия «Мозгового штурма» для самых умных. Потратьте несколько минут и хорошенько поразмыс- лите над задачей.
дальше 4 595 чтение и запись файлов D o gg y I D О б ъ е к т О б ъ е к т D o g g y I D О б ъ ект D o g О б ъ е к т Lis t < D o g > О б ъ е кт K e n n e l “ Fido” “Spike” При сериализации объекта также сериализуются все объекты, на которые он ссылается... ...а та кже вс е объекты, на которые ссылаются они, и всеобъекты, на которые ссылаютсяэти объекты, и т.д. Небеспокойтесь —этозвучит сложно, новсе проис- ходит автоматически. C# начинаетс объекта, которыйвыхотите сериализовать, ипроверяет егополяв поискахсвязанныхобъектов. Найдятакойобъект,онпо- вторяет аналогичную операцию. Врезультате вфайлбудут записанывсе объекты наряду со всей информацией, необходимой C# для полного восстановления при десериализации объекта. Когда вы приказываете се- риализовать объ ект Kennel (Конура), C# находит все поля, содержащие ссылки на другие объекты. Одним из полей объекта Kennel является коллекция List<Dog>, в кото- рой содержатся два объекта Dog (Собака). Их также требуется сериа- лизовать. Оба объекта Dog содер- жат ссылки на объект DoggyID (Идентифи- катор собаки) и объ- ект Collar (Ошейник). Они тоже должны быть сериализованы вместе с каждым объектом Dog. Объекты DoggyID и Collar завершают цепочку ссы- лок — они не ссылаются на другие объекты. Breed.Mutt 6 лет 18 фунтов 11 дюймов Breed.Beagle 4 года 32 фунта 14 дюймов О б ъ ект D o g О б ъ е к т C o l l a r О б ъ е к т C o l l a r Группаобъектов, связанных друг с другом по ссыл- кам, иногда называ- ется «графом».
596 глава 10 json сохраняет состояние объекта в текстовом виде Использование JsonSerialization для сериализации объектов Вашивозможностинеограничены чтением изаписью строктекста вфайлы. Механизмсериализации JSON позволяетпрограммам копировать целые объекты в строки(которые затем можно записать в файлы!), читать их из файла... и все это в нескольких строках кода! Посмотрим, как это работает. Начнемссоздания консольного приложения. Напишите несколько классов для графа объектов. ДобавьтеперечислениеHairColor и классы Guy,Outfit и HairStyle для нового консольного при- ло жения : class Guy { public string Name { get; set; } public HairStyle Hair { get; set; } public Outfit Clothes { get; set; } public override string ToString() => $"{Name} with {Hair} wearing {Clothes}"; } class Outfit { public string Top { get; set; } public string Bottom { get; set; } public override string ToString() => $"{Top} and {Bottom}"; } enum HairColor { Auburn, Black, Blonde, Blue, Brown, Gray, Platinum, Purple, Red, White } class HairStyle { public HairColor Color { get; set; } public float Length { get; set; } public override string ToString() => $"{Length:0.0} inch {Color} hair"; } Создайте граф объектовдля сериализации. Теперь создайтенебольшойграф объектовдлясериализации:новыйсписокList<Guy>,содержа- щий указателина паруобъектовGuy.Добавьтев метод Mainследующий код,в котором инициа- лизатор коллекции иинициализаторыобъектов используютсядля построенияграфа объектов: static void Main(string[] args) { var guys = new List<Guy>() { new Guy() { Name = "Bob", Clothes = new Outfit() { Top = "t-shirt", Bottom = "jeans" }, Hair = new HairStyle() { Color = HairColor.Red, Length = 3.5f } }, new Guy() { Name = "Joe", Clothes = new Outfit() { Top = "polo", Bottom = "slacks" }, Hair = new HairStyle() { Color = HairColor.Gray, Length = 2.7f } }, }; } 1 2 О б ъ е к т L i s t < G u y > О б ъ е к т G u y “Joe” Red О б ъ е к т O u t f i t “t-shirt” “jeans” О б ъ е к т O u t f i t “polo” “slacks” О б ъ е к т G u y “B ob” Gr ay Сделайте э то!
дальше 4 597 чтение и запись файлов Используйте JsonSerializer для сериализации объектов в строку. Начнитес добавления директивы using в начало файла с кодом: using System.Text.Json; После этого весь граф сериализуется одной строкой кода: var jsonString = JsonSerializer.Serialize(guys); Console.WriteLine(jsonString); Запуститеприложениеивнимательнорассмотритето,что оновыводитна консоль: [{"Name":"Bob","Hair":{"Color":8,"Length":3.5},"Clothes":{"Top":"t-shirt","Bott om":"jeans"}},{"Name":"Joe","Hair":{"Color":5,"Length":2.7},"Clothes":{"Top":"p olo","Bottom":"slacks"}}] Таквыглядит граф объектов, сериализованный в JSON— формат обменаданными, удобный для восприятия человеком. Иначе говоря, сложные объекты сохраняются встроковомвиде,понятном длячеловека. Так какэтотформат может читатьсячело- веком, мы видим внем всечастиграфа: имена ипредметы одежды («Bob», «t-shirt») кодируютсявстроках,а перечислениякодируютсякаких целочисленныезначения. Используйте JsonSerializer для десериализации JSON в граф объектов. Итак, унасесть строка сграфомобъектов, сериализованнымвJSON;этустрокуможно десериализовать(т. е.использовать еедлясозданияновыхобъектов).JsonSerializer такжепозволяет сделать это в однойстроке кода. Добавьте следующий код в метод Main: var copyOfGuys = JsonSerializer.Deserialize<List<Guy>>(jsonString); foreach (var guy in copyOfGuys) Console.WriteLine("I deserialized this guy: {0}", guy); Сновазапустите свое приложение. Оно десериализуетданные из строкиJSONивы- водитих на консоль: I deserialized this guy: Bob with 3.5 inch Red hair wearing t-shirt and jeans I deserialized this guy: Joe with 2.7 inch Gray hair wearing polo and slacks 3 4
598 глава 10 сериализуем один тип, десериализуем другой Разберемся подробнее, как работает JSON. Вернитесь к приложению с графом объектов Guy и замените строку, которая сериализует граф в строку, следующим фрагментом: var options = new JsonSerializerOptions() { WriteIndented = true }; var jsonString = JsonSerializer.Serialize(guys, options); Этот код вызывает перегруженный метод Json.Serializer.Serialize, который по- лучает объект JsonSerializerOptions с параметрами сериализации. В данном случае включается запись JSON в виде текста с отступами — другими слова- ми, добавляются разрывы строки и пробелы, которые упрощают чтение JSON человеком. Снова запустите программу. Вывод должен выглядеть так: А теперь разберемся, что же мы видим: ‘ JSONначинается и заканчивается квадратными скобками []. Так в JSON сериализуются списки. Список чисел выглядит так: [1, 2 , 3 , 4]. ‘ Этот конкретный блок JSON представляет собой список с двумя объекта- ми. Каждый объект начинается и заканчивается фигурными скобками {}; присмотревшись к JSON, вы увидите, что во второй строке располагает- ся открывающая фигурная скобка {, в предпоследней — закрывающая }, а в середине блока — строка }, за которой следует строка с{. Так вJSON представляются два объекта — в данном случае два объекта Guy. ‘ Каждый объект содержит набор разделенных двоеточиями ключей и зна- чений, соответствующих свойствам сериализованного объекта. Напри- мер, "Name": "Joe" представляет свойство Name второго объекта Guy. ‘ Свойство Guy.Clothes — это ссылка , указывающая на объект Outfit. Оно представляется вложенным объектом со значениями Top и Bottom. JSONпод увеличительным стеклом [ { "N ame": "Bob ", "H air": { "Colo r": 8 , "Leng th": 3.5 }, "C lothe s": { "Top" : "t- shirt ", "Bott om": "jean s" } }, { "N ame": "Joe ", "H air": { "Colo r": 5 , "Leng th": 2.7 }, "C lothe s": { "Top" : "po lo", "Bott om": "slac ks" } } ] При использовании JsonSerializer для сериализации графа объектов в JSON генерируется (относительно) удобочитаемый текст, представляющий данные каждого объекта.
дальше 4 599 чтение и запись файлов JSON включает только данные, но не конкретные типы C# Просматриваяданные JSON, вывидитепонятные длячеловекаверсии данныхвашихобъектов:строки «Bob» и«slacks», числа 8и3.5идаже спискии вложенныеобъекты. Ачего мыневидимпри просмотре данныхJSON? ВJSONотсутствуютимена типов.Загляните вфайлJSON—выне увидите в нем такихимен классов, какGuy, Outfit, HairColor или HairStyle, и даже имен базовыхтиповint, stringилиdouble. Делов том, что JSONсодер- жит только данные,а JSONSerializerделаетвсевозможное длядесериализации данныхв найденныесвойства. Чтобыубедиться вэтом,добавьтевпроект новыйкласс: class Dude { public string Name { get; set; } public HairStyle Hair { get; set; } } Добавьте следующий код в конец метода Main: var dudes = JsonSerializer.Deserialize<Stack<Dude>>(jsonString); while (dudes.Count > 0) { var dude = dudes.Pop(); Console.WriteLine($"Next dude: {dude.Name} with {dude.Hair} hair"); } Снова запустите свой код. Так как JSON содержит только список объектов, JsonSerializer.Deserialize пре- спокойноразместитихвстеке(илиочереди, илимассиве,илилюбойдругойразновидности коллекций). ТаккакDude содержит открытые свойства Name и Hair, соответствующие данным, десериализация за- полняетвседанные,которые может заполнить. Результат выглядиттак: Next dude: Joe with 2.7 inch Gray hair hair Next dude: Bob with 3.5 inch Red hair hair Данные десериализуются из списка List объектов Guy в стек объектов Dude. Воспользуемся JsonSerializer для анализа преобразования строк в JSON. До- бавьте следующий код в консольное приложение и запишите, что каждая стро- ка кода выведет на консоль. Последняя строка сериализует эмодзи со слоном. Console.WriteLine(JsonSerializer.Serialize(3)); Console.WriteLine(JsonSerializer.Serialize((long)-3)); Console.WriteLine(JsonSerializer.Serialize((byte)0)); Console.WriteLine(JsonSerializer.Serialize(float.MaxValue)); Console.WriteLine(JsonSerializer.Serialize(float.MinValue)); Console.WriteLine(JsonSerializer.Serialize(true)); Console.WriteLine(JsonSerializer.Serialize("Elephant")); Console.WriteLine(JsonSerializer.Serialize("Elephant".ToCharArray())); Console.WriteLine(JsonSerializer.Serialize(" ") ); Панель эмодзи использовалась в главе 1 для ввода эмодзи. Возьми в руку карандаш
600 глава 10 определение типов по байтам Класс JsonSerializer сериализует только открытые свойства (не поля), и ему необходим конструктор без параметров. Помните класс SwordDamage из главы 5? Его свойство Damage имеет при- ватный set-метод: public int Damage { get; private set; } Также он имеет конструктор, получающий параметр int: public SwordDamage(int startingRoll) JsonSerializer сериализует объекты SwordDamage без малейших проблем. При попытке десе- риализаци JsonSerializer выдаст исключение — по крайней мере в приведенном примере. Если вы захотите сериализовать объекты, сохраняющие свое состояние в полях или приватных свойствах либо использующие конструкторы с параметрами, придется создать преобразо- ватель. Эта тема более подробно рассматривается в документации по сериализации в .NET Core: http://docs.microsoft.com/en-us/dotnet/standard/serialization. JsonSerializer использует set- методы объекта при десериа- лизации, и если объект имеет приватный set-метод, присвоить значение не удастся. И последнее! Мы продемонстрировали базовый процесс реализации на примере JsonSerializer. Осталась еще пара подробностей, о которых вам необходимо знать. ¢ Сериализацией называется запись полного состоя- ния объекта вфайл или строку. Обратный процесс — чтение состояния объекта из файла или строки — на- зывается десериализацией. ¢ Группа объектов, связанных друг с другом по ссыл- кам, иногда называется графом. ¢ При сериализации объекта сериализуется весь граф объектов, на которые он ссылается, чтобы все объ- екты могли быть сериализованы вместе. ¢ Класс JsonSerializer содержит статический метод Serialize, сериализующий граф объектов в JSON, и статический метод Deserialize, создающий экземпляр графа объектов на основании сериализованных дан- ных JSON. ¢ Данные JSON могут читаться человеком (большей частью). Значения сериализуются в виде простого текста; строки заключаются в "кавычки" , а другие литералы (например, числа и логические значения) кодируются без кавычек. ¢ В JSON массивы значений заключаются в квадрат- ные скобки []. ¢ В JSON объекты заключаются в фигурные скобки {}, а компоненты и их значения представляются в виде пар «ключ/значение», разделенныхдвоеточием. ¢ JSON не сохраняет имена конкретных типов (string, int, имена отдельных классов). Вместо этого «умные» классы (такие, как JsonSerializer) прикла- дывают максимум усилий к тому, чтобы сопоставить данные стипом, в который они десериализуются. Будьте осторожны! КЛЮЧЕВЫЕ МОМЕНТЫ
дальше 4 601 чтение и запись файлов Следующий шаг: углубленный анализ данных Ранеевы написалибольшой объем кода, в котором использовались типы значений:int,bool,double,а такжесоздавались объекты,хранящиеданные вполях. Пришловремяразобратьсявпроисходящем наболее низкомуровне. Воставшейсячастиэтойглавы выувидите, какиереальныебайты использу- ются вC# и.NET дляихпредставления. Вотчтонампредстоит сделать. Мы исследуем, как строки C# кодируются в Юникоде — . NE T использует Юникод для хранения символов и текста. Мы запишем данные в двоичной форме, а затем прочитаем их и посмотрим, какие данные были записаны. Мы построим программу вывода шестнадцатеричного дампа, которая поможет в анализе битов и байтов в файлах. 7 byte variables 333311110810810172 0000: 45 6c 65 6d 65 6e 74 61 Elementa 0005:72792c206d792064ry,myd 0010:6561722057617473earWats 0015: 6f 6e 21 on!
602 глава 10 доступность игр и приложений Д ост упност ь Как бы вы играли в свою любимую игру, если бы вы плохо видели? Что, если двигательные нарушения или огра- ниченные двигательные возможности усложняют использование контроллера? Цельдоступности — гарантировать, что построенные вами программы спроектированы таким образом, чтобы они могли использоваться людьми с огра- ниченными возможностями и нарушениями. Вы стремитесь к тому, чтобы сделать вашу игру доступной для всех, независимо от любых проблем со здоровьем. Существует ряд подходов к тестированию игр, которые стоит продуматьв началепроектирования и построения игр: • «Погодите, что? Есть слепые игроки?» Да! А вы думали, что «видео» в видеоиграх означает, что эти игры недоступны для людей с нарушениями зрения? Потратьте несколько минут на поиск в YouTube по строке «blind gamer» и посмотрите ролики, показывающие, как люди с нарушениями зрения (включая полностью слепых) демонстрируют серьезные игровые навыки. • Чтобы вы как разработчик могли сделать свою игру доступной, очень важно понять игроков с нарушениями. Что вы узнали из просмотра этих роликов? • Наблюдая за слепыми игроками, мы узнали, что они используют игровые звуки, чтобы ориентироваться в про- исходящем. В файтинге разные приемы могут сопровождаться разными звуками. В платформенной игре враги, двигающиеся к игроку, могут издавать характерные звуки. Вы также узнали, что хотя некоторые игры предостав- ляют адекватные звуковые ориентиры, другие этого не делают — и лишь немногие игры спроектированы для того, чтобы полностью слепым игрокам хватало звуковой информации для игры. • С другой стороны, у многих игроков встречаются нарушения слуха, поэтому важно не ограничиваться только звуковыми ориентирами. Попробуйте поиграть в вашу любимую игру c отключенным звуком. Не мешает ли это передаче важной информации игроку? Существуют ли визуальные ориентиры, сопровождающие аудио? Субти- тры для диалогов? Убедитесь в том, что в вашу игру можно играть без звука. • Один из двенадцати мужчин (и одна из двухсот женщин) страдают той или иной формой дальтонизма, т . е. не- способностью различать цвета (кстати, к их числу принадлежит и один из авторов этой книги!). Многие высокобюд- жетные игры включают режим для дальтоников, в котором выполняется нетривиальная регулировка цветов. Для улучшения доступности игр для дальтоников можно использовать цвета, заметно контрастирующие друг сдругом. • Многие геймеры страдают от нарушений моторики, начиная с травмы от повторяющихся деформаций до па- ралича. Игроки, которые не могут работать с традиционными устройствами ввода (такими, как мышь или кла - виатура), могут пользоваться вспомогательным оборудованием (например, устройством для отслеживания направления взгляда или модифицированным геймпадом). Если вы хотите привлечь таких игроков к игре, реа- лизуйте механизм привязки клавиатуры, чтобы игроки могли создать профиль, в котором специальные клавиши соответствуют разным элементам управления игры. • Возможно, вы заметили , что многие игры начинаются с предупреждения о возможных приступах. Дело в том, что многие игроки с эпилепсией чувствительны к свету, и мерцание или вспышки могут стать причиной при- ступа. Хотя предупреждения важны, но можно пойти дальше. Для разработчика важно приложить сознательные усилия к тому, чтобы понять суть проблемы и избегать световых вспышек, мерцания и других визуальных эффектов, которые с наибольшей вероятностью приводят к приступам. Прочитайте редакционную статью ре- цензента видеоигр Кэти Вайс (Cathy Vice) о ее личном опыте игрока с эпилепсией (https://indiegamerchick. com/2013/08/06/the-epilepsy-thing). Поддержка доступности часто добавляется «задним чис- лом», но ваши игры — как, впрочем, и любые другие про- граммы! — будут более качественными, если вы будете учитывать этот аспект с самого начала. Разработка игр... и не только
дальше 4 603 чтение и запись файлов Строки C# кодируются в Юникоде Выиспользовалистрокис того момента, когда ввели текст"Hello, world!" вначале главы1.Таккакстрокиинтуитивнопонятны, мы неанализировалиихине сталиподроб- ноописывать,какониработают.Носпроситесебя...что жепредставляет собой строка? СтрокаC#представляет собойнаборсимволов,доступныйтолькодля чтения.Таким об- разом,если выпосмотрите настроку«Elephant», хранящуюся впамяти,выувидите символы ‘E’,‘l’, ‘e’,‘p’, ‘h’,‘a’, ‘n’ и‘t’.Атеперь задайтесь вопросом...чтоже представляетсобойсимвол? Символпредставляет собой знак,представленный вЮникоде — отраслевом стандар- те кодирования символов или преобразованияихвбайты, позволяющем хранить их впамяти,передавать по сети, включать в документы ивыполнять практически любые возможныеоперации, — и вы всегда гарантированно получитеправильныесимволы. Этотаспектособенно важен, если задуматься над тем, сколько символов существует в разных алфавитах. СтандартЮникод поддерживает свыше150 скриптов(наборов символов для конкретных языков),включая нетольколатинский(с26буквамианглий- ского алфавита и такими модификациями, какé иç),но и скрипты для многих языков извсехстран.Списокподдерживаемых скриптовпостояннорастет, таккакконсорциум Юникода каждый год расширяет его (текущий список доступен по адресу http://www. unicode.org/standard/supported.html). ВЮникодеподдерживаетсяещеодинважныйнабор символов: эмодзи. Всеэмодзи,от подмигивающего смайлика( )до неизменно популярнойкучидерьма( ),являются символамиЮникода. СимволыЮникода могут препятствовать работе вспомогательных технологий Доступность чрезвычайно важна. Мы считаем, что очень важно поднять тему доступности, потому что, как и во многих темах из разделов «Разра- ботка игр... и не только», из нее можно извлечь урок, относящийся к разра- ботке любых программ. Вероятно, вы видели в социальных сетях сообщения с эмодзи или другими «необычными» символами — курсивными, жирными или перевернутыми. На некото- рых платформах для этого используются символы Юникода, и в этом варианте они могут создать серьезные проблемы для вспомогательных технологий. Возьмем следующее сообщение в социальной сети: I’m👏using👏hand👏claps👏to👏empha si ze 👏poi nts На экране такое сообщение выглядит нормально. Однако Экранный диктор Windows или Voice- Over в macOS может прочитать сообщение, заменяя пробелы словами «аплодирующие руки»). Возможно, вы видели «шрифты» следующего вида: Экранный диктор либо прочитает описание как «малое m Fraktur полужирный», а для остальных символов — что -то вроде «буква n из скрипта», либо вообще проигнорирует их. Все это может оказаться совершенно бесполезным для читателей, использующих систему чтения Брайля. Выходит, возможности вспомогательных технологий ограниченны? Вовсе нет — они справляются со своим делом. Все это реальные имена символов Юникода, а вспо - могательные технологии точно описывают текст. Помните об этих примерах, читая сле- дующую часть этой главы, — о ни по м огут вам лучше понять, как работает Юникод. Вигреспо- иском пар из главы 1 симво- лы Юникода об- рабатывались точно так же, как и любые другие символы Юникода. Будьте осторожны!
604 глава 10 Когда я сериализовал эмодзи со слоном в JSON, получилось что-то вроде "\UD83D\UDC18" — наверняка это как-то связано с Юникодом? каждый символ уникален Каждому символу Юникода, включая эмодзи, ставится в соответствие уникальное число, называемое кодовым пунктом. Числовоепредставление символа Юникода называется кодовым пунктом. ПолныйсписоквсехсимволовЮникода доступенпо адресуhttps://www.unicode. org/Public/UNIDATA/UnicodeData.txt. Это оченьбольшой текстовыйфайл, в котором каждый символ Юникода представлен отдельной строкой. Загрузитеего, проведите поиск строки «ELEPHANT», и вы найдете строку следующего вида: 1F418;ELEPHANT. Чис- ло 1F418 представляет шестнадцатеричное значение. Шестнадцатеричные числа состоят из цифрот0 до 9 и буквA–F, и со значениями из Юникода (и двоичными значениями вообще) в шестнадцатеричнойформе частобы- ваетпрощеработать,чем вдесятичной.Чтобы создать шестнадцатеричный литералвC#,добавьтепрефикс0x:0x1F418. 1F418являетсякодовым пунктомUTF-8для эмодзисослоном. UTF -8 —самый распространенныйспособ кодирования символов вЮникоде(т. е. представ- ления ихв числовом виде).Эта кодировка переменной длины используетот 1до 4байт. Вданномслучае используются3байта:0x01(или1),0xF4(или244) и 0x 18 (или 24). Темнеменееэтонетезначения, которыебудутвыведенысредствамисериали- зацииJSON.Выводитсяболее длинное шестнадцатеричное число:D83DDC18. Деловтом, что типcharвC#используеткодировкуUTF-16,в которой кодовые пункты состоятиз одного или двух2-байтовых чисел.Кодовыйпунктэмодзи со слоном в UTF-16 имеет вид 0xD83D 0xDC18. UTF -8 намного популярнее UTF-16, особенно воВсемирнойпаутине, такчто припоиске кодовые пункты UTF-8 встретятсявам с гораздо большей вероятностью,чем UTF-16 . UTF-8 — кодировка переменной длины, используемая в большин- стве веб-страниц и во многих системах. Она позволяет хранить символы с представлением их одним, двумя, тремя или большим количеством байтов. UTF-16 — к одировка фиксированной длины, которая всегда использует одно или два 2-байтовых числа. .NET хранит значения char в памяти в виде значений UTF-16.
дальше 4 605 чтение и запись файлов Поддержка Юникода в Visual Studio ВоспользуемсяVisual Studio и посмотрим, как IDEработает с символами Юникода. В главе1было показано, что впрограммах можно использовать эмодзи. На что еще способна IDE?Вернитесь кре- дактору кода и введите следующую команду: Console.WriteLine("Hello "); ЕсливыработаетевWindows,откройтеприложениеТаблица символов.НаMac нажмитеCtrl+⌘,что- быоткрыть приложениеCharacterViewer.Найдитебукву«шин» (ש) изиврита, скопируйтееевбуфер. Установитекурсорвконец строкимеждупробелом икавычкойивставьте скопированнуюбукву«шин». Хм-м,происходитчто-то странное: Вы заметили, что курсор оказался слева от вставленнойбуквы? Что ж, продолжим. Не щелкайте вIDE— оставьтекурсор вего текущей позиции,затем переключитесь на приложениеТаблица симво- лов/Character Viewer, найдите и скопируйтебукву иврита «ламед» (ל). Переключитесь обратно вIDE: убедитесь втом,что курсор всеещенаходится слева от«шин», и вставьте «ламед»: ПривставкеIDE добавляет новый символ слева от «шин». Теперь найдите буквы иврита «вав» (ו) и«мем» (ם).ВставьтеихвIDE — символы вставляются слева откурсора: IDEзнает,что текст наивритечитается справаналево,иведетсебясоответствующим образом. Щелк- ните,чтобы выделить текст рядомс началом команды,имедленно перетащитекурсор вправо,чтобы выделить Helloисимволы םולש.Внимательно понаблюдайте затем, что происходит, когдавыделение достигает букв иврита. «Шин» пропускается, а символы выделяются справа налево — именно этого иожидает читатель,знающий иврит. Как Таблица символов Windows, так и Character Viewer в macOS позволяют искать символы Юникода и копировать их в буфер.
606 глава 10 .n et использует юникод . NET использует Юникод для хранения символов и текста Дватипа C# для хранения текста исимволов — string и char — хранят свои данные в памяти в Юникоде. Когда этиданныезаписываютсявфайлввиде байтов, каждое изэтихчиселзаписываетсявфайл.Попро- буемразобратьсявтом,какименноданные Юникода записываютсявфайл. Создайтеновое консольное приложение.ДляисследованияЮникода мывоспользуемсяметодамиFile.WriteAllBytesи File.ReadAllBytes. Запишите обычную строку в файл и прочитайте ее. Добавьте следующийкодвметодMain— методFile.WriteAllTextиспользуетсядлязаписистроки «Eureka!» в файл с именем eureka.txt (а значит, потребуется директива using System.IO;). За- темсоздаетсяновыймассивбайтовс именемeurekaBytes,внего читается содержимоефайла ивыводятся все прочитанныебайты: File.WriteAllText("eureka.txt", "Eureka!"); byte[] eurekaBytes = File.ReadAllBytes("eureka.txt"); foreach (byte b in eurekaBytes) Console.Write("{0} ", b); Console.WriteLine(Encoding.UTF8 .GetString(eurekaBytes)); Мывидим,что на выходезаписываютсяследующиебайты: 69 117 114 101 97 33. Последняя строка вызывает метод Encoding.UTF8 .GetString, который преобразует массив байтов, содер- жащий символы в кодировкеUTF-8, в строку. Теперь откройтефайл вБлокноте(Windows) или TextEdit (Mac). Файл содержит текст «Eureka!». Добавьте код записи байтов в виде шестнадцатеричных чисел. Прикодировании данныхчастоиспользуетсяшестнадцатеричнаясистема;опробуем этувозмож- ность. Добавьте в конец метода Main следующий код, который записываетте жебайты, используя {0:x2}дляформатирования каждого байта как шестнадцатеричного числа: foreach (byte b in eurekaBytes) Console.Write("{0:x2} ", b); Console.WriteLine(); ФрагментприказываетWrite вывести параметр0(первый после выводимой строки) в виде двухсимвольногошестнадцатеричного кода.Следовательно,те же 7байтбудут записанывшест- надцатеричномформатевместо десятичного: 45 75 72 65 6b 61 21. Измените первуюстроку, чтобывместо«Eureka!» выводилисьбуквыиврита “םולש”. Недавно вы добавили в другую программу текст на иврите םולשиз Таблицы символов (Wi) или Character Viewer(Mac).Закомментируйтепервуюстроку методаMain изаменитеееследующим кодом, которыйвыводит«םולש» (вместо«Eureka!») вфайл.Мыдобавили дополнительныйпараметр Encoding.Unicode, чтобы данные записывались в UTF-16(классEncodingпринадлежит простран- ствуименSystem.Text, поэтому в начало необходимо добавить директивуusing System.Text;): File.WriteAllText("eureka.txt", "םולש", Encoding.Unicode); Сновазапустите коди присмотритесь квыводу:ff fe e9 05 dc 05 d5 05 dd 05. Первые два символа «FF FE» в Юникоде сообщают отом, что далее идет строка, состоящаяиз2-байтовыхсим- волов. Остальныебайтысодержатбуквы иврита, но вобратномпорядке байтов, такчтоU+05E9 соответствуют байты e9 05. Откройтефайл вБлокноте или вTextEdit и проверьте результат. 1 2 3 Метод ReadAllBytes возвращает ссылку на новый массив байтов, содержащий все байты, прочитанные из файла. В шестнадцатеричной системе используются циф- рыот0до9ибуквыотAдоFдляпредставления чисел с основанием 16, так что шестнадцатерич- ное число 6B соответствует десятичному 107. Сделайте это!
дальше 4 607 чтение и запись файлов Используйте JsonSerializer для анализа кодовых пунктов UTF-8 и UTF-16. Когда высериализовалиэмодзи сослоном, классJsonSerializerсгенерировалпоследовательность \uD83D\uDC18, которая, какмы теперь знаем,является4-байтовым кодовым пунктомUTF-16 в шестнадцатеричном виде. Теперь опробуем его сбуквой иврита «шин». Включите директиву using System.Text.Json; в начало приложения, после чего добавьте следующую строку: Console.WriteLine(JsonSerializer.Serialize("ש ")); Снова запустите приложение. На этотраз выводится значение из двух шестнадцатеричных байтов, «\u05E9» — кодовыйпунктбуквы иврита «шин» вUTF-16.Также онявляетсякодовым пунктом той жебуквы вUTF-8. Нопостойте: мыузнали,что кодовыйпунктэмодзи со слоном в UTF-8равен 0x1F418 ион от- личается от кодового пунктаUTF-16(0xD83D0xDC18).Что происходит? Оказывается, большинство символовс2-байтовымикодовымипунктамиUTF-8имеют такие же кодовые пункты вUTF-16 .Но когда мы добираемся до значенийUTF-8,требующих 3 иболее байт(которые включаютуже знакомые эмодзи, использовавшиеся в книге), они начинают различаться. Таким образом, хотя буква иврита «шин» представлена кодовым пунктом UTF-8 иUTF-16, эмодзи со слоном имеет кодовыйпункт0x1F418 вUTF-8 и0xD8ED0xDC18 вUTF-16. Используйте служебные последовательности Юникод а дл я коди ровани я . Добавьте следующиестрокив методMain, чтобы записать эмодзисо слономв два файла с использованиемслужебных последовательностейUTF-16иUTF-32: File.WriteAllText("elephant1.txt", "\uD83D\uDC18"); File.WriteAllText("elephant2.txt", "\U0001F418"); Сновазапуститесвоеприложение,затем откройтеобафай- ла вБлокнотеиливTextEdit. Вфайледолженсодержаться правильный символ. 4 5 Служебные последовательности \u и включение Юникода в строки Когда мы сериализовали эмодзи со слоном, класс JsonSerializer сгенерировал для эмодзи 4-бай- товый кодовый пункт UTF-16 \uВ83В\uDC18 в шестнадцатеричной форме. Это объясняется тем, что строки JSON и C# используют служебные последовательности UTF-16 — и как вы- ясняется, те же служебные последовательности используются JSON. Символы с 2-байтовыми кодовыми пунктами (такие, как ש) представляются служебной после- довательностью \u, за которой следует шестнадцатеричный кодовый пункт (\u05E9); символы с 4-байтовыми кодовыми пунктами (такие, как ) представляются последовательностью \u и двумя старшими байтами, за которыми следует \u и младшие 2 байта (\uD83D \uDC18). В C# также поддерживается другая служебная последовательность: \U (с U в ВЕРХНЕМ РЕГИСТРЕ), за которой следуют 8 шестнадцатеричных байт, позволяет встроить кодо- вый пункт UTF-32, длина которого всегда равна 4 байтам. Это еще одна кодировка Юникода, и она по-настоящему полезна, потому что вы можете преобразовать UTF-8 в UTF-32 про- стым дополнением шестнадцатеричного числа нулями, — т аки м образом, кодовый пункт <> в UTF-32 равен \U000005E9, а для он равен \U0001F418. Вы использовали служебные последовательности UTF- 16и UTF-32для создания эмодзи, но метод WriteAllText записывает файл в UTF-8. Метод Encoding.UTF8.GetString, использованный на шаге1, пр еоб разует м ассив б айт ов с данными, закодированными в UTF-8, в строку.
608 глава 10 Таккаквседанныев конечномитогекодируютсябайтами, есть смысл представлятьсебефайл как один большой массивбайтов...а выуже знаете,какчитать и записывать массивы байтов. C# может использовать массивы байтов для перемещения данных byte[] greeting; greeting = File.ReadAllBytes(filename); Hello!! 7 байтовых переменных 721011081081113333 7 байтовых переменных 333311110810810172 !!olleH Array.Reverse(greeting); File.WriteAllBytes(filename, greeting); Этот код создает массив бай- тов, открывает входной поток и читает текст "Hello!" в бай- ты с 0-го по 6-й массива. Статический метод клас- са Array переставляет байты в обратном порядке. Мы ис- пользуем его только для того, чтобы показать, что изме- нения в массиве байтов точно записываются в файл. Байты переставлены в обратном порядке. Когда программа записы- вает массив байтов в файл, символы текста тоже пере- ставляются в обратном порядке. Эти числа явля- ются значениями символов строки "Hello!" в Юникоде. запись байтов Перестановка байтов "Hello!" работает только из-за того, что длина каждого символа составляет один байт. А вы сможете разобраться, почему он не работает для םולש?
дальше 4 609 чтение и запись файлов Использование BinaryWriter для записи двоичных данных Данныеразличных типов — всеэтистроки, char, int иfloat — перед записью в файл можно пре- образовыватьвмассивыбайтов, но это крайне монотонная иутомительнаяработа. Поэтому в.NETпоявилсяочень полезный классBinaryWriter,автоматически преобразующий данные изаписывающийих вфайл. Вам нужно толькосоздать объектFileStreamипередать его конструк- тору класса BinaryWriter (они принадлежат пространству имен System.IO, поэтому необходимо добавить директивуusing System.IO;).Послеэтоговызывается методзаписиданных.Давайте потренируемся в использовании класса BinaryWriter для записи двоичных данных в файл. Создайтеконсольное приложение иданныедлязаписи вфайл: int intValue = 48769414; string stringValue = "Hello!"; byte[] byteArray = { 47, 129 , 0, 116 }; float floatValue = 491.695F; char charValue = 'E'; 1 ЧтобыиспользоватьBinaryWriter, сначала необходимооткрытьновый потокметодом File.Create: using (var output = File.Create("binarydata.dat")) using (var writer = new BinaryWriter(output)) { 2 ВызовитеметодWrite. Прикаждомвызове этого метода новые байтыбудут добав- лятьсяв конец файла сзакодированной версией данных, переданныхв параметре: writer.Write(intValue); writer.Write(stringValue); writer.Write(byteArray); writer.Write(floatValue); writer.Write(charValue); } 3 Каждый вызов Write преоб- разует в байты одно значение, которое передается объекту FileStream. Передать можно любой тип значения, и он будет закодирован автоматически. П о дс казка: строки могут иметь раз- ную длину, поэтому каждая строка начинается с числа, которое сообщает .NET ее длину. BinaryWriter использует UTF-8 для кодирования строк, а в UTF-8 все символы "Hello!" имеют кодовые пункты UTF, состоящие из 1 байта. За- грузите файл Unicode.txt на сайте unicode.org (URL-адрес приводился ранее) и восполь- зуйтесь им для нахождения кодовых пунктов каждого символа. Если вы используете File.Create, он создает новый файл — а если файл существует, он будет уничтожен, и на его месте будет создан новый. Метод File.OpenWrite откры- вает имеющийся файл и начинает записы- вать новую информацию поверх старой. Используем написанныйранеекоддля чтениятолько что за- писанных данных: byte [] dat aWri tte n = Fil e.R ead AllB yte s("b ina ryd ata. dat "); foreach (byte b in dataWritten) Console.Write("{0:x2} ", b); Console.WriteLine(" - {0} bytes", dataWritten.Length); Запишитерезультат в пустых полях внизу. Вы сможете опреде- лить, какиебайтысоответствуют каждомуиз пяти вызововwriter. Write(...)? Мы пометили фигурной скобкой группу байтов, соот- ветствующую каждому вызову; это поможет вам определить, какие байты вфайле соответствуютданным, записанным приложением. 4 Объект FileStream запи- сывает байты в конец файла. Объект StreamWriter тоже кодирует данные, но он специализируется на тексте — по умолчанию используется UTF-8. ____________ __ __ __ __ __ __ __ __ __ __ __ __ __ __ - __ _ bytes intValue stringValue byteArray floatValue char Val ue Сдела й те э то! Возьми в руку карандаш
610 глава 10 intValue stringValue byteArray floatValue ch arValu e 8629e8020648656c6c6f212f810074f6d8f54345 20 __ __ __ __ __ __ __ __ __ ____________________ __ - ___ bytes Переменная типа char содержит символ Юнико- да, а ‘E’ занимает всего 1 байт — он кодируется в фор- ме U+0045. Использование BinaryReader для чтения данных Класс BinaryReader работает аналогично классу BinaryWriter. Вы создаете поток, присоединяете к нему объектBinaryReader и вызываете его методы... Но BinaryReader не знает, что за данные содержатся вфайле! И знать не может. Значениетипаfloat 491.695F преобразовалось вd8f54345.Но этиже байты соответствуютвполне допустимому значению типа int — 1 140 185334, аследовательно, необходимоявно сообщить BinaryReader, данные какоготипа читаютсяизфайла. Добавьте в программуследующийкод, которыйбудетчитать только что записанныеданные. Значения float и int при записи в файл занимают по 4 байта. Если бы вы ис- пользовали типы long или double, они занимали бы по 8 байт. В калькуляторах для Windows и Mac предусмотрены режимы для програм- мистов, преобразующие эти байты из шестнадцатеричной системы в деся- тичную, чтобы вы могли преобразо- вать их обратно в значения из массива. Первый байт в строке равен 6; это длина строки. Чтобы най- ти каждый из символов «Hello!», используйте Таблицу символов — слово начинается с U+0048 и за- канчивается U+0021. Начнем с создания объектов FileStream и BinaryReader: using (var input = File.OpenRead("binarydata.dat")) using (var reader = new BinaryReader(input)) { 1 СообщитеBinaryReader, данныекакого типа должны читатьсяпривызоверазныхметодов: int intRead = reader.ReadInt32(); string stringRead = reader.ReadString(); byte[] byteArrayRead = reader.ReadBytes(4); float floatRead = reader.ReadSingle(); char charRead = reader.ReadChar(); 2 Для каждого типа значения в BinaryReader определен от- дельный метод, возвращающий данные правильного типа. Боль- шинству из них параметры не нужны, хотя метод ReadBytes() использует параметр, сооб- щающий объекту BinaryReader количество читаемых байтов. Данные,прочитанныеизфайла,выводятсяна консоль: Console.Write("int: {0} string: {1} bytes: ", intRead, stringRead); foreach (byte b in byteArrayRead) Console.Write("{0} ", b); Console.Write(" float: {0} char: {1} ", floatRead, charRead); } Результат,которыйвыводитсяна консоль: int: 48769414 string: Hello! bytes: 47 129 0 116 float: 491.695 char: E 3 Не верьте нам на слово. Замените строку, в кото- рой читается float, вызовом метода ReadInt32 (тип floatRead нужно будет заменить на int). И вы сами уви- дите, какие данные читаются из файла. сплав данных Возьми в руку карандаш Решение
дальше 4 611 чтение и запись файлов В: Ранее в этой главе, когда я запи- сывал слово «Eureka!» в файл, а потом читал его обратно, каждый символ пред- ставлялся однимбайтом. Почему жедля записи букв на иврите в слове םולש тре- буетсяпо2байта? Ичтоэтозасимволы «FF FE» в начале файла? О: Здесь проявляютсяразличия между двумя тесно связанными кодировками Юникод.Латинскимбуквам(включаябуквы анг лий ско го алфав ит а), цифрам, знак ам пре- пи нания, а та кже неко тор ым с тан дарт ным символам(фигурнымскобкам,амперсандам и другимзнакам, которые вы видите на кла- виатуре) соответствуютнебольшиезначения в Юникоде— от 0до127.Онисоответствуют очень старой кодировкеASCII, которая по- явилась еще в 1960-х годах, а кодировка UTF-8разрабатывалась сцелью сохранения обратной совместимости сASCII.Файл, со- стоящий только из этих символовЮникода, содержит только этибайты, и ничего более. Все усложняется, когда на сцену выходят символы с большими значениями. В одном байте могут храниться только значения от 0до255.Вдвух байтах уже можно хранить числаот 0до65536—в шестнадцатеричной системе счисления это FFFF.Файлдолжен иметь возможность сообщить программе, которая будет открыватьфайл, о присут- ствиисимволов с высокими значениями;для этого в начало файла помещается зарезер- вированная последовательность символов: FFFE.Этотакназываемаяотметка порядка байтов. Обнаружив ее, программа узнает, что все символы кодируются 2 байтами (такимобразом,E кодируетсяс начальным нулем в виде00 45). В: Почему эти символы называются отметкой порядка байтов? О:Вернитесь к коду, который записывал буквы םולש в файл, а затем выводил за- писанные байты. Вы увидите, что байты в файле следуютвобратном порядке. Напри- мер, кодовый пункт שбыл записан в файл в видеE905.Этот порядок называется пря- мым — он означает, что менее значимый байтзаписывается первым. Вернитесь ккоду, который записывалWriteAllText, и измените третий аргумент с Encoding.Unicode на Encoding.BigEndianUnicode. Темсамымвы указываете, что данные записываются в об- ратномпорядке, — с н о в а за пустив программу, вы увидите, что байты следуют впорядке 05E9. Также изменится и отметка порядка байтов: FE FF.По ней Блокнот илиTextEdit опред еляет , к ак с л еду ет инте рпрети ров ать байты в файле. В: Почему после методов File.Read­ AllText и File.WriteAllText мы не исполь- зуем блок using и не вызываем метод Close? О: Класс File содержит несколько очень полезных статических методов, которые автоматически открывают файл, читают илипишутданные, азатемавтоматически закрывают его.Кромеметодов ReadAllText и WriteAllText, также существуют методы ReadAllBytes и WriteAllBytes, читающие массивы байтов, и методы ReadAllLines и WriteAllLinesдлячтенияизаписимассивов строк; каждая строка в массиве становится отдельнойстрокойв файле.Всеэтиметоды автоматически открывают и закрывают по- токи, так что все операции в файле можно вы пол нить о дной кома ндой . В: Если в классе FileStream имеются методы чтения и записи,то зачем нужны классы StreamReader и StreamWriter? О: Класс FileStream используется для чтения и записи байтов вдвоичный файл. Егометодыработаютсбайтамиимассивами байтов. Но есть и программы, работающие только с текстовыми данными, и в таких случаях классыStreamReader иStreamWriter становятся очень удобными. Методы этих классов созданы специально для чтения или записи строк текста. Без них при не- обходимости прочитать строку текста из файлавампришлосьбысначала прочитать массив байтов,а затемписать цикл,ищущий в этоммассивезнаки разрыва строк,такчто иногда эти классы сильно облегчают жизнь. Если вы записываете строку, состоящую из символов Юникода с малыми значениями (например, букв латинского алфавита), для каждого символа записывается 1 байт. Для записи символов Юникода с большими значениями (например, эмодзи) выделяются 2 и более байта. часто Задава ем ые вопросы
612 глава 10 69736e277420746869732066756e3f 0a Дамп позволяет просматривать байты в файлах Шестнадцатеричным дампомданных(hexdump)называетсяпредставление содержимогофайла в шест- надцатеричном виде. Это представление часто используется для анализа внутренней структурыфайла. Каквыясняется, шестнадцатеричная система хорошо подходит длявывода байтов изфайла. Байт в шест- надцатеричной системе записывается двумя шестнадцатеричными цифрами:значениябайта лежат в диа- пазоне от 0до 255, или от 00до ffв шестнадцатеричной системе. Такое представление позволяет уместить большой объем данных в относительно малом пространстве, в формате, которыйупрощает выявление закономерностей.Двоичные данные полезно разбивать на строки длиной 8, 16 или32байта, потому что размер многихдвоичныхданныхобычно кратен4,8,16или32...какувсехтиповC#.(Например, тип intза- нимает4байта.)Шестнадцатеричныйбайт позволяетточнопонять,изкакихчастей строятсяэтизначения. Создание шестнадцатеричного дампа простого текста Начнемсознакомоготекста, записанного буквамилатинского алфавита: When you have eliminated the impossible, whatever remains, however improbable, must be the truth. - Sherlock Holmes Сначаларазбейте текстна сегменты из16символов,начинаяс первых16: When you have el Затем преобразуйте каждый символ в тексте в кодовый пунктUTF-8 .Так как все латинские символы используют1-байтовые кодовые пунктыUTF-8, каждый из нихбудет представленшестнадцатеричным числом из2 цифрот00 до 7F.Каждая строка дампа будетвыглядеть так: 0000:5768656e20796f75--206861766520656c When you have el Повторяйтедо техпор,пока небудут выведенывсе 16-символьныесегменты изфайла: 0000:5768656e20796f75--206861766520656c When you have el 0010:696d696e61746564--2074686520696d70 imin ated the i mp 0020:6f737369626c652c--2077686174657665 ossi ble, whate ve 0030:722072656d61696e--732c20686f776576 r remains, howev 0040:657220696d70726f--6261626c652c206d er i mprob able, m 0050:7573742062652074--6865207472757468 ust be the truth 0060:2e202d2053686572--6c6f636b20486f6c .- Sherl ock H ol 0070: 6d 65 73 0a -- mes. Этоиестьнаш дамп.Существует многоразныхпрограмм вывода дамповдля разныхоперационныхсистем,и все они выводят слегка отличающиеся результаты. Каж- дая строка этой конкретной программы представляет 16символоввходных данных, использованных для ге- нерирования дампа; в началестроки выводится смеще- ние, а в конце — текст всех символов сегмента . Другие приложениявывода шестнадцатеричных дамповмогут выводить информацию в другом формате(например, с визуализацией служебных последовательностей или выводом информациив десятичнойсистеме). Шестнадцатеричный дамп пред- ставляет собой шестнадцатерич- ное представление данных в фай- ле или в памяти. Он может стать очень полезным инструментом для отладки двоичных данных. Смещение сегмен- та (или позиция в файле), записанная в виде шестнадца- теричного числа. Первые 8 байт 16-байтово- го сегмента Послед- ние 8 байт 16-байтового сегмента Символы тек- ста, включен- ные в дамп Разделитель упроща- ет чтение строки
дальше 4 613 чтение и запись файлов Использование StreamReader для вывода шестнадцатеричного дампа Построим приложение для выдачи шестнадцатеричного дампа. Прило- жение читает данныеизфайла при помощиStreamReader ивыводитдамп на консоль. Мы воспользуемся методом ReadBlock класса StreamReader, которыйчитаетблок символоввсимвольный массив;привызовезадается количество читаемых символов. Метод читает либо заданноеколичество символов, либовесьостатокфайла(если не осталосьтребуемогоколичества символов).Так как приложениедолжно выводить данныепо 16символам, размер читаемого блока будет составлять 16 символов. СоздайтеновоеконсольноеприложениесименемHexDump.Прежде чем добавлять код, запуститеприложение, чтобысоздать папкус двоичнымфай- лом. Припомощи Блокнота илиTextEditсоздайте текстовыйфайл сименем textdata.txt, добавьте в него текст и поместите в папкус двоичным файлом. Нижеприведен кодметода Main — он читает файлtextdata.txtи записывает на консоль шестнадцатеричный дамп. Не забудьте добавитьв началофайладирективуSystem.IO;. static void Main(string[] args) { var position = 0; using (var reader = new StreamReader("textdata.txt")) { while (!reader.EndOfStream) { // Прочитать следующие 16 байт из файла в массив байтов var buffer = new char[16]; var bytesRead = reader.ReadBlock(buffer, 0, 16); // Записать шестнадцатеричную позицию (или смещение), затем двоеточие и пробел Console.Write("{0:x4}: ", position); position += bytesRead; // Записать шестнадцатеричное значение каждого символа в массив байтов for(vari=0;i<16;i++) { if (i < bytesRead) Console.Write("{0:x2} ", (byte)buffer[i]); else Console.Write(" "); if (i == 7) Console.Write("-- "); } // Записать символы в массив байтов var bufferContents = new string(buffer); Console.WriteLine(" {0}", bufferContents.Substring(0, bytesRead)); } } } Запуститеприложение. На консоль выводится шестнадцатеричныйдамп: 0000:456c656d656e7461--72792c206d792064 Elementary, my d 0010:6561722057617473--6f6e21 ear Watson! Форматная конструкция «{0:x4}» преобразует чис- ловое значение в четырех- значное шестнадцатеричное число; таким образом, 1984 преобразуется в строку "07c0". Свойство EndOfStream объекта StreamReader возвращает false, пока в файле остаются симво- лы, которые могут быть прочитаны методом. Цикл пере- бирает сим- волы и выво- дит каждый символ в выходную с тр оку. Метод ReadBlock читает следующие символы из входного потока в байто- вый массив (иногда назы- ваемый буфером). Чтение выполняется в блокиру- ющем режиме, т. е . метод продолжает работать и не возвращает управление до тех пор, пока программа не прочитает все запрошен- ные символы или не будут исчерпаны все входные символы. Метод String.Substring возвращает часть строки. Первый параметр задает начальную позицию (в дан- ном случае начало строки), а во вт ор ом пер ед ается кол и чество символов, включаемых в подстро- ку. КлассString содержит пере- груженный конструктор, который получает массив char в параметре и преобразует его в строку.
614 глава 10 передача имени файла в командной строке Использование Stream.Read для чтения байтов из потока Программа прекрасно работаетс текстовымифайлами. Скопируйтефайлbinarydata.dat, записанный ранеес использованием BinaryWriter, в папку приложения, а затем измените приложениедля чтения файла: using (var reader = new StreamReader("binarydata.dat")) Запуститеприложение.Наэтотразвыводитсядругаяинформация— ноне совсемправильная: 0000:fd29fd020648656c--6c6f212ffd0074fd ?) ?Hel lo! /? t? 0010: fd fd 43 45 -- ?? CE Символытекста(«Hello!») выглядят нормально.Но сравните выводсрешениемупражнения «Возьмитевруку карандаш» — с байтами что-то не так. Похоже, некоторыебайты(86, e8, 81,f6,d8иf5)замененыбайтомfd.Деловтом, чтоклассStreamReaderпредназначендля чтения текстовых файлов, поэтому ончитает только7-разрядныезначения,т.е. байты со значениями до 127(7F вшестнадцатеричной записиили1111111 вдвоичной). Исправимпроблему— чтениембайтовнапрямуюизпотока.Изменитеблокusing, чтобыв нем использовалсявызовFile.OpenRead,который открываетфайливозвращаетFileStream. СвойствоLengthклассаStream используетсядлятого,чтобы прочитать всебайтывфайле, а метод Readчитаетследующие16 байт вбуфер массивабайтов: using (Stream input = File.OpenRead("binarydata.dat")) { var buffer = new byte[16]; while (position < input.Length) { // Прочитать до следующих 16 байт из файла в массив байтов var bytesRead = input.Read(buffer, 0, buffer.Length); Остатоккода остаетсянеизменным,кроместроки,в которой задается значение bufferContents: // Записать символы в массив байтов var bufferContents = Encoding.UTF8.GetString(buffer); Класс Encoding использовался ранее в этой главе для преобразования массива байтов встроку.Массивбайтовсодержит1байт насимвол —т .е . он являетсядействительнойстро- койв кодировкеUTF-8.Это означает,что его содержимое можно преобразоватьметодом Encoding.UTF8 .GetString. Так как класс Encoding принадлежит пространству именSystem. Text, в начало файла необходимо добавить директиву using System.Text;. Сновазапустите приложение. На этотраз оно выводит правильные байты вместо того, чтобы заменять ихбайтомfd: 0000:8629e8020648656c--6c6f212f810074f6 ?) ?Hel lo! /? t? 0010: d8 f5 43 45 -- ?? CE Осталосьещеодно,что можно сделать длячисткивывода. Многие программы шестнадца- теричного дампа заменяют нетекстовые символыввыводе точками.Добавьтеследующую строку в конец цикла for: if (buffer[i] < 0x20 || buffer[i] > 0x7F) buffer[i] = (byte)'. '; Сновазапуститеприложение— на этотраз вопросительныезнакизаменяются точками: 0000:8629e8020648656c--6c6f212f810074f6 .). ..He llo! /..t . 0010: d8 f5 43 45 -- . .CE В решении упражнения «Возьми- те в руку карандаш» эти байты были равны 81иfb,но StreamReader заменил их на fd. Мы использовали явный тип вместо var, чтобы наглядно показать, что в приложении использу- ется поток, а именно FileStream (расширяю- щий Stream). Метод Stream.Read получает три аргумента: массив байтов для чтения данных (buffer), начальный индекс в массиве (0) и количество читаемых байтов (buffer.Length). Теперь ваше при- ложение читает из файла все байты, не только символы те кст а.
дальше 4 615 чтение и запись файлов Аргументы командной строки Большинство программ вывода шестнадцатеричного дампа — утилиты, предназначенные для запуска из командной строки. Чтобы вывести дамп файла,пользователь передаетего имяпро- граммев аргументе командной строки — например, C:\> HexDump myfile.txt. Изменим приложение так, чтобы в нем использовались аргументы командной строки. Когда высоздаетеконсольноеприложение,C# предоставляет доступ каргументам команднойстроки встроковом массивеargs,передаваемом методу Main: static void Main(string[] args) Изменим метод Main, чтобы он открывал файл и читал содержимое из потока. Метод File. OpenRead получаетимяфайла в параметре, открываетфайл для чтения и возвращает поток с содержимымфайла. Измените следующие строки в методе Main: static void Main(string[] args) { var position = 0; using (Stream input = File.OpenRead(args[0])) { var buffer = new byte[16]; int by tes Read ; // Прочитать до следующих 16 байт из файла в массив байтов while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0) { Чтобы воспользоваться аргументами командной строки,следует изменить вIDE свойства от- ладки ивключить передачу командной строкипрограмме. Щелкните правой кнопкой мыши на проектеврешении,а затем: ‘ В Windows — выберите команду Properties, щелкните на категорииDebug и введитеимя файла длявыдачи дампа вполе Application arguments (либо полное имя,либо имяфайла впапкес двоичнымфайлом). ‘ ВmacOS — выберитекоманду Options,раскройтеразделRun>>Configurations, щелкните на Default ивведитеимя файла вполеArguments. Теперь приотладке приложениямассивargsбудет содержать аргументы, заданные внастройках проекта.Убедитесь втом,чтопринастройке аргументовкоманднойстрокибылозаданоправильноеимя. Запуск приложения из командной строки Такжеможнозапустить приложениеиз командной строки, заменив[имя_файла] именемфайла (полным именем илиименем файла в текущем каталоге): ‘ В системеWindowsVisual Studio строит исполняемыйфайл в папкеbin\Debug(втой, вкоторойразмещается файл для чтения), так что исполняемыйфайл можно запустить прямо из этойпапки. Откройтеокно командной строки,перейдитевкаталогbin\Debug командойcdи выполните команду HexDump[имя_файла]. ‘ НаMacнужнобудет построитьавтономное приложение.Откройте окно терминала,перей- дите в папку проекта и выполните следующую команду: dotnet publish -r osx-x64. В выводе будет присутствовать строка следующего вида: HexDump -> /path-to-binary/osx-x64/ publish/ Откройте окно терминала,перейдитепо выведенному полному пути командой cd,выполните команду ./HexDump [filename]. Приложение получает аргументы командной строки в параметре args и передает их ме- тоду GetInputStream. Также необходимо уда- лить строку с объявле- нием bytesRead и пер- вую строку блока while, в которой вызывается inputRead для потока. Будьте внима- тельны: щелкнуть нужно именно на проекте, аненаре- шении.
616 глава 10 загрузите следующий проект со страницы github ¢ Юникод — отраслевой стандарт кодирования символов, т. е . преобразования их в байты. Каждому из миллио- на с лишним символов Юникода назначается кодовый пункт, т . е . уникальный числовой идентификатор. ¢ Большинство файлов и веб-страниц кодируется в UTF­8 — кодировке переменной длины, в которой символы кодируются одним, двумя, тремя или че- тырьмябайтами. ¢ C# и .NET используютUTF-16дляхранения символов и текста в памяти, при этом строка интерпретируется как коллекция символов, доступная только для чтения. ¢ Метод Encoding.UTF8.GetString преобразует мас- сив байтов UTF-8 в строку. Encoding.Unicode пре- образует массив байтов, закодированный в UTF-16 , в строку, а Encoding.UTF32 преобразует массив бай- тов UTF-32. ¢ Служебные последовательности \u используются длявключенияЮникода в строкиC#.Последователь- ность \u содержит кодировку UTF-16 , а \U содержит UTF­32 —4 -байтовую кодировку переменной длины. ¢ StreamWriter и StreamReader хорошо работают с тек- стом, но обрабатывают лишь немногие символы за пределами латинского набора символов. Для чте- ния двоичных данных следует использовать классы BinaryWriter и BinaryReader. ¢ Метод StreamReader.ReadBlock читает символы в буферный массив байтов. Он выполняется в бло- кирующем режиме, т. е. продолжает работать и не возвращает управление до тех пор, пока не будут прочитаны все запрошенные символы или не будут исчерпаны все данные для чтения. ¢ File.OpenRead возвращает FileStream, а метод FileStream.Read читает байты из потока. ¢ Метод String.Substring возвращает часть строки. Класс String содержит перегруженный конструктор, который получает массив char в параметре и преоб- разует его в строку. ¢ C# предоставляет доступ к аргументам командной строки консольных приложений в строковом масси- ве args, передаваемом методуMain. Упражнение: Hide and Seek Вследующемупражнениимы построим приложение,в котором выисследуетедом ииграетевпрятки сигроком,управляемым компьютером. Процедураразмещения комнат проверит вашинавыкиработыс коллекциямииинтерфейсами.Затембудет реализована сериализация состояния игры вфайле, чтобы вы могли сохранить и загрузить его. Сначала вы исследуете вирту- альный дом, перемещаясь из комнаты в комнату и исследуя предметы в каждой комнате. Затем добавляется компьютерный игрок, который ищет ваше убежище. Посмотри- те, сколько ходов ему для этого понадо- бится! Перейдите к репозиторию GitHub данной книги и загрузите PDF-файл проекта: https://github.com/head-first-csharp/fourth-edition КЛЮЧЕВЫЕ МОМЕНТЫ
Лабораторный курс Unity No5 Head First C# Лабораторный курс Unity No 5 617 Создавая сцену в Unity, вы создаете виртуальный 3D-мир, в котором перемещаются персонажи вашей игры. Но в большинстве игр объекты окружающей обстановки не контролируются игроком напрямую. Как же эти объ- екты определяют свое место в сцене? В лабораторных работах No 5 и 6 вы познакомитесь с системой поиска путей и навигации — изощренной системой на базе искусственного интеллекта, которая позволяет создавать персонажей, способных находить путь в построенных вами мирах. В этой лабораторной работе мы построим сцену из объектов GameObject и используем навигацию для перемещения персонажей по сце не. Отслеживание лучей (raycasting) будет использовать- ся для написания кода, реагирующего на геометрию сцены, захвата ввода и использования его для пере- мещения объекта GameObject в точку, в которой щелк- нул игрок. Что не менее важно, вы потренируетесь в написании кода C# с классами, полями, ссылками и другими аспектами, которые были представлены в пре- дыдущих главах. Лабораторный курс Unity No 5 Отслеживание лучей https://github.com/head-first-csharp/fourth-edition
618 глава 10 Лабораторный курс Unity No 5 Создание нового проекта Unity и начало создания сцены Прежде чем браться заработу, закройте все проектыUnity, открытые в данный момент. Также закройтеVisualStudio — Unityоткроет среду за вас. Создайте новый проектUnity с использованием3D-шаблона, выберите макетWide, чтобы он соот- ветствовал приведенным снимкамэкранов, и присвойтеемуимя UnityLabs5and6, чтобы иметьвозможностьвернуться к немувбудущем. Начнитессоздания игровойобласти,вкоторойбудет ориентироваться игрок. Щелкнитеправой кнопкой мыши в окнеHierarchy и создайте плоскость (GameObject>>3D Object>>Plane). Присвойте объекту Plane имя Floor. Щелкнитеправой кнопкой мыши на папкеAssets в окне Project и создайте в ней папку с именем Materials. Затем щелкните на только что созданной папке Materials и выберите команду Create>>Material. Присвойте новому материалу имяFloorMaterial.Пока оставим этот материал простым — только назначим ему цвет. ВыберитеобъектFloor вокне Project, затем щелкните на беломполесправа отсвойстваAlbedo в окнеInspector. ВокнеColor выберитецвет пола на внешнем кольце. На снимке мы выбрали цветс кодом 4E51CB — вы можетеввести это число вполеHexadecimal. Перетащите материал из окнаProject на объектGameObject Plane в окне Hierarchy. Пол должен окраситься в выбранный вами ц вет. , Плоскость Plane представляет собой плоский квадратный объект с размерами 10Í10 единиц (поплоскости X-Z) ивысо- той 0 единиц (по оси Y). Unity создает пло- скость так, что ее центр находится в точке (0, 0, 0). Центральная точка плоскости опре- деляет ее положение в сцене. Как и другие объекты, плоскость можно перемещать по сцене при помощи окна Inspector или воспользоваться инструментами, изменя- ющими ее позицию и угол поворота. Также можно изменить масштаб, но поскольку плоскость не имеет высоты, изменять можно только масштаб по осям X и Z — лю- бое положительное число, введенное для масштаба по оси Y, игнорируется. Объекты, созданные из меню 3D Object (плоскости, сферы, кубы, цилиндры и не- сколько других простых фигур), называ- ются примитивными объектами. Чтобы большеузнать о них, откройтеруковод- ство Unity из меню Help и найдите страни- цу«Primitive and placeholder objects». Не торопитесь и откройте эту страницупрямо сейчас. Прочитайте, что в ней говорится о плоскостях, сферах, кубах и цилиндрах. Плоскость не имеет измерения Y.Что произойдет, если увеличить масштаб по оси Y? А если мас- штаб по оси Y будет отрицательным? Нулевым? Мозговой штурм Подумайте над вопросом и попробуйте предполо- жить результат. Затем при помощи окна Inspector опробуйте разные значения масштаба Y и посмо- трите, работает ли плоскость так, как ожидалось. (Потом не забудьте вернуть исходное значение!) Вы можете воспользоваться пипеткой для того, что- бы выбрать образец цвета в любой точке экрана.
Лабораторный курс Unity No5 Head First C# Лабораторный курс Unity No 5 619 Настройка камеры ВдвухпоследнихлабораторныхработахUnity выузнали, чтообъектGameObjectфактически является «контейнером» для компонентов, а главная камера (MainCamera) содержит три компонента: Transform, Camera и Audio Listener. Это вполне логично, потому что все, что требуется от камеры, — находиться в определенном месте и записывать то, что она видит и слышит. Взгляните накомпонентTransformв окне Inspector. Обратите внимание: поле Position содержит значение (0, 1 , –10).Щелкнитена надписи Z встрокеPosition иперетащите ее вверх-вниз. Выувидите, каккамера двигаетсявперед-назад в окне сцены. Обратите внимание на четыре линии перед камерой.Онипредставляютобласть просмотра(viewport), т. е.видимую область на экране игрока. Переместитекамерупо сцене,повернитеееприпомощиинструментовMove(W)иRotate (E),как это делалось с другимиобъектамиGameObjectв сцене. ОкноCamera Preview обнов- ляетсявреальном времени,показывая,что видиткамера. Следите за окномCamera Preview приперемещениикамеры. Привходе ивыходе пола изобластипросмотракамерысоздается вп еча тление, что о н дв ижетс я. Воспользуйтесь контекстным меню в окне Inspector для сброса компонента Transform главной камеры. Позициякамерыи ееуголповоротавозвращаются кзначениям(0,0,0).Камера пересекаетплоскость вокнеScene. Направимкамерувниз.Щелкните нанадписи Xрядомс Rotation,попробуйте перетащитьее вверх-вниз .Выувидите, чтообласть просмотра вокнеCamera Preview двигается.Задайтекамереуголповорота по осиX,равный90,в окне Inspector,чтобы направить камерувертикально вниз. Вы заметите, что окно Camera Preview стало пустым, и это понятно, потому что камера направлена вниз под бесконечно тонкую плоскость. Щелкните на надписи YкомпонентаTransform и перетащитевверх,пока неувидитевсю плоскость вокне Ca mer a Previ ew. Выберите объект Floor в окнеHierarchy.Обратите внимание:окно Camera Preview исчезает — оно отображаетсятолькопривыделеннойкамере. Вытакже можете пере- ключаться между окнамиScene иGame,чтобыувидеть то, что видиткамера. Припомощи компонента TransformобъектаPlane в окне Inspectorназначьтеобъекту GameObject Floor масштаб (4,1, 2), чтобы его длина была вдвое больше ширины. Таккакширина ивысота объектаPlaneравны10Í10единиц,с этим масштабом его длинабудетсоставлять 40единиц, аширина —20единиц. Плоскостьсноваполностью перекрывает область просмотра,поэтому сдвиньте камерувверх по осиY, пока вся плоскость неокажется вполе зрения. Вы може- те пере- ключать- ся между окнами Scene и Game, что- бы увидеть, что в дан- ный мо- мент видит камера. Верхняя половина камеры возвышается над плоско- стью.
620 глава 10 Лабораторный курс Unity No 5 Создание объекта GameObject для игрока Нампонадобится игровой персонаж,которымбудетуправлять игрок(далеебудем называть его про- сто «игроком»).Мы создадим простую человекоподобнуюфигурус цилиндром вместо тела исферой вместо головы.Чтобыубедитьсявтом, чтониодин объект внастоящиймомент не выделен,щелкните на сцене(илив пустом месте)вокнеHierarchy. Создайте объектGameObjectCylinder(3DObject>>Cylinder) — всерединесценыпоявитсяцилиндр. Присвойте емуимяPlayer, затемвыберитекоманду Resetв контекстном менюкомпонента Transform, чтобыобъектубылиприсвоенызначения поумолчанию. ЗатемсоздайтеобъектGameObject Sphere (3DObject>>Sphere).Присвойте емуимяHeadивыполните сбросегокомпонентаTransform, какбыло сделано с цилиндром.Каждыйобъектбудет представленотдельной строкой вокнеHierarchy. Нонамнужны недва отдельных объектаGameObject,а одинобъектGameObject,которымуправляет один сценарийC#.Для такихситуацийвUnity существует концепцияродительскихсвязей.Щелкните на объектеHead в окнеHierarchy и перетащите его на Player. ОбъектPlayerстановится родителем Head.Теперь объектGameObject Head вложенвPlayer. Выделите объектHead в окнеHierarchy. Онбыл создан в точке (0,0,0), как и все созданные вами сферы. Вы видите контур сферы, но сама сфера не видна, потому что она скрыта плоскостью и ци- линдром. Используйте компонентTransform в окне Inspector, чтобы изменить позицию Y сферы на 1.5.Теперь сфера располагается над цилиндром,как раз в подходящем месте для головы игрока. Выделите объектPlayerвокне Hierarchy.ТаккакегопозицияYравна0, поло- винацилиндраскрыта плоскостью.Задайте егопозициюYравной1.Цилиндр поднимаетсянад плоскостью.Обратитевниманиенато,чтосфераHead подня- ласьвместесним. ПриперемещенииPlayer объектHead перемещаетсявместе с ним,потомучто перемещение родительского объектаGameObjectприводит кперемещениюегопотомков — более того, любоеизменение, внесенное вком- понент Transform, автоматическиприменяетсяк потомкам. Например, при масштабированииродительскогообъектаегопотомки тоже масштабируются. Переключитесь вокноGame, — фигуркаигроканаходитсявсерединеигро- войобласти. Когда вы изме- няете компонент Transform объ- екта GameObject, у которого есть вложенные по- томки, потомки будут переме- щаться, вращать- ся и масштаби- роваться вместе с ним.
Лабораторный курс Unity No5 Head First C# Лабораторный курс Unity No 5 621 Знакомство с системой навигации Unity Одна изосновныхопераций, выполняемыхввидеоиграх, — перемещениераз- личных объектов. Игроки,персонажи,враги,предметы, препятствия... все эти объекты могутдвигаться. По этой причине вUnity существует сложная система поиска путейи навигации набазе искусственногоинтеллекта, которая помогаетобъектам GameObject перемещаться по сцене. Мы воспользуемся системойнавигациидлятого,чтобы игрокдвигался кцели. Системапоискапутейинавигациипозволяетперсонажамосмысленноискать путь вигровом мире. Чтобывоспользоватьсяею,необходимосообщить Unity, куда игр о к мо жет идти: ‘ Во-первых, необходимо точно сообщить Unity, где разрешено пере- мещаться игроку. Для этого создается объект навигационной сетки NavMeshс полнойинформацией обо всех областях сцены,в которых разрешеноперемещение:склоны,лестницы,препятствияидаже точки, называемые внесеточнымизвеньями, вкоторыхвыполняютсяособые действияигрока(например, открытиедвери). ‘ Во-вторых, добавьте компонентNavMeshAgent к каждому объекту GameObject, которыйдолжен выполнятьнавигацию. Этот компонент автоматически перемещает объектGameObjectпо сцене,используяИИ для поиска самого эффективного пути к цели с обходом препятствий (ивозможно, других компонентов NavMesh Agent). ‘ Иногда навигация по сложным объектамNavMeshтребуетсерьезных вычислений.Вот почемувUnityпредусмотренафункцияBake, которая позволяет настроить NavMesh заранее и провести предварительную обработку геометрической информации, чтобы повысить эффектив- ность работы агентов. Сцена Unity, по которой должен перемещаться игрок. В сцене есть пол, лестница, ведущая на возвышение, и несколько препятствий, которые игрок должен обходить. Пан ель N avMesh Display от- крывается при открытии окна Navigation Послепредварительной обработки сетка NavMesh ото- бражается в виде синей «накладки» на всех объектах GameObject. Теперь вы можетедобавить искусственный интеллект к любому объекту GameObject; для этого доста- точно добавить компонент NavMesh Agent, который спосо- бен автоматически построить путь к любой точке сцены. Unity предо- ставляет слож- ную систему поиска путей и навигации, которая мо- жет переме- щать объекты GameObject по сцене в реаль- ном времени с поиском эф- фективных пу- тей и обходом препятствий.
622 глава 10 Лабораторный курс Unity No 5 Создание сетки NavMesh Создадим сетку навигации NavMesh, которая состоит только из плоскостиFloor. Для этого мы воспользуемся окном Navigation. Выберите команду AI>>Navigation из менюWindow, чтобы добавить окноNavigation в рабочее пространствоUnity. Оно должно появиться в видевкладки на одной панели с окном Inspector. Затем в окнеNavigation включите для объекта GameObject Floor параметры navigationstatic иwalkable. ‘ НажмитекнопкуObject вверхнейчасти окнаNavigation. ‘ Выберите плоскостьFloor вокнеHierarchy. ‘ Установите флажок Navigation Static. Тем самым вы приказываете Unity включать Floor припредварительном построенииNavMesh. ‘ ВыберитеWalkable израскрывающегосясписка Navigation Area. Это сообщает Unity, что плоскость Floor является поверхностью, по которойможет перемещаться любойобъект GameObject с компонентом NavMeshAgent. Нажмите кнопкуObject, чтобы пометить объекты GameOb ject в сцен е к ак на- вигационно-статические; это о значает, что о ни яв ля ются ча стью NavM esh и остаю тся неподвижными. Плоскость Floor объявляется как проходимая; NavMesh Agent будет знать, как продолжить маршрут. Так какединственной проходимой областью в игребудет пол, на- стройка раздела Object на этом завершена. Дляболее сложных сцен с большим количествомпроходимыхповерхностей или непроходимых препятствий каждый отдельный объект GameObject должен быть снабженсоответствующейпометкой. ЩелкнитенакнопкеBake вверхнейчастиокнаNavigation,чтобыпро- смотреть возможные параметрыпостроениянавигационнойсетки. ТеперьщелкнитенадругойкнопкеBakeвнижнейчастиокнаNavigation. Надпись на ней ненадолго заменится текстом Cancel, а потом снова вернется кBake. А вы заметили, что в окне Scene что-то изменилось? Попробуйте переключатьсямеждуокнамиInspector и Navigation. Когда окно Navigation активно, в окне Sceneвыводитсянавигационная сетка: NavMeshвыделяется каксиняя«накладка» на объектахGameObject, ко- торые входят в предварительно построенную сеткуNavMesh.Вданном случае выделяетсяплоскость, котораябылапомечена какнавигацион- но-статическая ипроходимая. Подготовка сетки NavMesh за вер шена . Щелкните на кнопке Bake, что- бы сгенерировать NavMesh.
Лабораторный курс Unity No5 Head First C# Лабораторный курс Unity No 5 623 Автоматическая навигация в игровой области Добавим компонентNavMesh Agent к объекту GameObject Player. Вы- делите Player в окнеHierarchy, вернитесь к окнуInspector, щелкните на кнопке Add Component и выберите команду Navigation>>NavMeshAgent. Высота тела (цилиндр) составляет2 единицы, а высота сферы (голова) составляет1единицу,поэтому высота агента должна бытьравна3едини- цам —задайтеHeightзначение 3.Теперь компонентNavMeshAgent готов к перемещению объекта GameObject Player по навигационной сетке. Создайте папку Scripts и добавьте сценарий с именем MoveToClick.cs. Сценарийпозволяетигрокущелкнуть влюбойточкеигровой областии переместитьобъектGameObjectв этуточку.Выузнали оприватныхполях в главе 5.Сценарий будет использовать такое поле для хранения ссылки на NavMeshAgent.КодуGameObject понадобитсяссылка на агента, чтобы онмог сообщить агенту,куда следует перемещаться; мы вызываемметод GetComponent для полученияссылкии сохраняем еев приватном поле NavMeshAgent с именем agent: agent = GetComponent<NavMeshAgent>(); Системанавигациииспользует классы изпространства именUnityEngine. AI, поэтомувначалофайлаMoveToClick.cs необходимо добавить строку: using UnityEngine.AI; Сценарий MoveToClick выглядит так: public class MoveToClick : MonoBehaviour { pri vate Na vMe shAg ent ag ent; void Awake() { agent = GetComponent<NavMeshAgent>(); } void Update() { if (In put .Get Mou seB utto nDo wn( 0)) { Camera cameraComponent = GameObject.Find("Main Camera").GetComponent<Camera>(); Ray ray = cameraComponent.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, 100)) { a gent .Se tDe stin ati on( hit. poi nt); } } } } Перетащитесценарий на объектPlayer и запуститеигру.Во время выполнения игры щелкните в лю- бойточке плоскости. КомпонентNavMeshAgent перемещает вашего игрока кточкещелчка. Здесь сценарий обрабатываетщелчки мышью. Ме- тод Input.GetMouseButtonDown проверяет, нажал ли пользователь кнопку мыши, а аргумент0 указывает на то, что проверяется левая кнопка. Так как метод Update вызываетсядля каждого кадра, проверка нажатия кнопки мыши выполняется постоянно. В последней лабораторной работе Unity метод Start использовалсядля задания позиции объектаGameObject при его первом появлении. Однако существует еще один метод, который вызывается до метода Start вашего сценария. МетодAwake вызывается при создании объекта, тогда как Start вызывается при активизации сценария. Сценарий MoveToClick использует для инициализации поля метод Awake, а не Start. Поэкспериментируйте с полями Speed, Angular Speed, Acceleration и Stopping Distance в агенте NavMesh. Их мож- но изменять во время выполнения игры (но помните, что значения, измененные во время игры, не сохраняются). Что произойдет, если присвоить им очень большие значения?
624 глава 10 Лабораторный курс Unity No 5 В нескольких последних главах много говорилось оссылкахна объекты иссылочныхпеременных. Немногопоработаем с карандашом,чтобыне- которые идеи иконцепции,лежащиев основе ссылокнаобъекты,закре- пились в вашем мозгу. Добавьте следующее открытое поле в класс MoveToClick: public Camera cameraComponent; Перейдите вокноHierarchy, щелкните на объекте Playerинайдитеновоеполе Main Cameraв компоненте Move To Click (Script). Перетащите объект MainCamera из окна Hierarchy на поле Camera Component компонента Move ToClick (Script) объектаGameObjectPlayer: Закомментируйте следующую строку: Camera cameraComponent = GameObject.Find("Main Camera").GetComponent<Camera>(); Снова запустите игру.Она всеравноработает!Почему? Подумайте ипопробуйте найти ответ.Запишите еговнизу. Да! Здесь используется чрезвычайно полезный инструмент, называемый отслеживанием лучей. Вовторой лабораторнойработе метод Debug.DrawRay помог нам разобраться втом, какработают3D-векторы; дляэтоговсценерисовалсялуч, начинающийся в точке (0,0,0).МетодUpdate вашегосценарияMoveToClickобычноделаетнечто похожее. Он использует методPhysics.Raycastдля«прокладки» луча(вроде того, который использовалсядляизучениявекторов) от камерыдоточки щелчкаи про- веряет,соприкасается ли луч сполом. Еслирезультатположительный,метод Physics.Raycast предоставляет точку пола, в которой обнаруженконтакт. Тогда сценарийзадаетполеточкиназначения компонента NavMeshAgent,врезуль- татечего NavMeshAgentавтоматически перемещает игрока куказанной точке. Вмоемсценарии вызывалисьметоды, в имени которых присутствует слово «Ray» (Луч).Я уже использовал лучи в первой лабораторной работе. Выходит, лучи также помогают двигаться игроку? Возьми в руку карандаш
Лабораторный курс Unity No5 Head First C# Лабораторный курс Unity No 5 625 Сценарий MoveToClick вызывает метод Physics.Raycast — чрезвычайно полезный инструмент, который Unity пре- доставляет для реакции на изменения в сцене. Метод проводит виртуальный луч в сцене и сообщает, столкнул- ся ли он с какими-либо объектами. Параметры метода Physics.Raycast сообщают, куда должен быть направлен луч и каково максимальное расстояние для его проведения: Physics.Raycast(направление_луча, out hit, макс_расстояние) Метод возвращает true, если столкновение будет обнаружено, или false в противном случае. Ключевое слово out используется для сохранения результата в переменной (точно так же, как это делалось с int.TryParse в нескольких последних главах. Разберемся поподробнее в том, как работает метод. Отслеживание лучей под увеличительным стеклом Итак, мы знаем параметры луча и можем вызвать методPhysics.Raycast, чтобы узнать точку столкновения: RaycastHit hit; if (Physics.Raycast(ray, out hit, 100)) { agent.SetDestination(hit.point); } Метод возвращает логическое значение и использует ключевое слово out — собственно, он работает точно так же, как int.TryParse. Если метод возвращает true, то переменная hit содержит точку пола, в которой происходит столкновение с лучом. Присваивание agent.destination приказывает NavMesh Agent начать перемещение игрока к точке столкновения луча с полом. Методу Physics.Raycast необходимо сообщить, кудадолжен быть направлен луч. Таким образом, прежде всего необходимо найти камеру, а конкретнее, компонент CameraобъектаGameObject Main Camera. Это делается примерно так же, как мы получали GameController в последней лабораторной работе Unity: GameObject.Find("Main Camera").GetComponent<Camera>(); Класс Camera содержит метод с именем ScreenPointToRay, который создает луч от текущей позиции камеры через точку(X,Y) наэкране. Метод Input.mousePosition предоставляет позицию (X,Y) экрана, в которой был сделан щелчок. В результате переменная ray содержит координаты, которыедолжны передаваться Physics.Raycast: Ray ray = cameraComponent.ScreenPointToRay(Input.mousePosition); Камера направлена вниз, прямоугольник являет- ся областью просмотра. Крестиком обозначена точка, в которой был сделан щелчок. Метод проводит луч длиной до 100 единиц, который начинается в текущей позиции ка- меры и проходит че- рез точку, в которой щелкнул пользователь. Луч пересекается с полом в этой точке.
626 глава 10 Лабораторный курс Unity No 5 В этом упражнении мы изменили класс MoveToClick и добавили поле для главной камеры вместо того, чтобы использовать методы Find и GetComponent. Выперетащили Main Camera,после чегомы задаливо- прос.Ваш ответ похожна наш? Снова запустите игру.Она всеравноработает!Почему? Подумайте ипопробуйте найти ответ.Запишите еговнизу. Мы будем повторно использовать сценарий MoveToClick в последующих лабораторныхработах, поэтому после того, как ответы будут записаны, верните сценарий к прежнему состоянию: удалите поле MainCamera и восстановите строку, задающую значениепеременной cameraComponent. Когда мой код вызвал метод mainCamera.GetComponent<Camera>, он вернул ссылку на объект GameObject. Я заменил ее полем и перетащил объект Main Camera из окна Hierarchy в окно Inspector, в результате чего поле было инициализировано ссылкой на тот же объект GameObject. Это дваразных способа присваивания пе- ременной cameraComponent ссылки на один и тот же объект, поэтому поведение программы не изменилось. ¢ ПлоскостьPlane представляет собой плоский квадрат- ный объект размером 10 Í10 единиц (по плоскости X-Z) и высотой 0 единиц (по оси Y). ¢ Вы можете перемещать главную камеру для изме- нения отображаемой части сцены;для этого измените компонент Transform (так же, как для любого другого объекта GameObject). ¢ При изменении компонента Transform объекта GameObject, у которого есть вложенные потомки, по- томки будут перемещаться, вращаться и масштабиро- ваться вместе с ним. ¢ Unity предоставляет систему поиска путей и нави- гации на базе искусственного интеллекта, которая может перемещать объекты GameObject по сцене в реальном времени с поиском эффективных путей и обходом препятствий. ¢ Объект NavMesh содержит всю информацию о прохо- димыхобластях сцены. Вы можете настроитьNavMesh заранее и заранее вычислить геометрические подроб- ности, необходимые для повышения эффективности работы агентов. ¢ Компонент NavMeshAgent автоматически перемеща- ет объект GameObject по сцене, используя искусствен- ный интеллект для нахождения самого эффективного пути к цели. ¢ Метод NavMeshAgent.SetDestination заставляет агента вычислить путь к новой позиции и начать дви- гаться к новой точке. ¢ Unity вызывает метод Awake вашего сценария при первой загрузке объекта GameObject, задолго до вы- зова метода Start сценария, но после создания экзем- пляров других объектов GameObject. Это место от- лично подходит для инициализации ссылок на другие объекты GameObject. ¢ Метод Input.GetMouseButtonDown возвращает true, если кнопка мыши нажата в данный момент. ¢ Метод Physics.Raycast выполняет отслеживание лу- чей, для чего он проводит виртуальный луч по сцене и возвращает true, если луч столкнулся с каким-либо объектом. Для возвращения информации об объектах, с которыми столкнулся луч, используется ключевое слово out. ¢ Метод ScreenPointToRay камеры создает луч, прохо- дящий через точку экрана. Объедините его с Physics. Raycast, чтобы определить, куда следует переместить игрока. КЛЮЧЕВЫЕ МОМЕНТЫ Возьми в руку карандаш
КАПИТАН ВЕЛИКОЛЕПНЫЙ СМЕРТЬ ОБЪЕКТА Head First C# Глава 11 Четыре доллара
628 глава 11 КАПИТАН ВЕЛИКОЛЕПНЫЙ,САМЫЙ ВЫДАЮЩИЙСЯ ОБЪЕКТ ОБЪЕКТ­ ВИЛЯ,ПРЕСЛЕДУЕТ СВОЕГО ВРАГА... ТЕБЕ НЕУЙТИ, ПРОЙДОХА ТЫ ОПОЗДАЛ! ПОКА МЫ ГОВО­ РИМ,НА ФАБРИКЕПОДНАМИ СО­ БИРАЕТСЯ МОЯ АРМИЯКЛОНОВ... ...ГОТОВАЯСЕЯТЬ ХАОС НА УЛИЦАХ ОБЪЕКТВИЛЯ! Я УНИЧТОЖУССЫЛКИ КЛОНОВОДНУЗА ОДНОЙ! БАХ!!
дальше 4 629 см ерть об ъекта КАПИТАНВЕЛИКОЛЕПНЫЙ ЗАГНАЛ ПРОЙДОХУ ВУГОЛ... ЧЕРЕЗ НЕСКОЛЬКОМИНУТ И ТЫ, И МОЯ АРМИЯ СТАНЕТЕ МУСОРОМ...БОЙТЕСЬ СБОР­ ЩИКА МУСОРА! ...НОСАМПОПАДАЕТ В ЛОВУШКУ. НЕУЖЕЛИ ЭТО КОНЕЦ КАПИТАНА ВЕЛИКОЛЕПНОГО? БАХ!!
630 глава 11 работа со сборщиком мусора Жизнь и смерть объекта Нижеприведен краткий обзор того, что мы знаем о жизни исмертиобъектов: ‘ При создании объекта среда CLR, которая запускает ваши приложения .NET иуправляет памятью, выде- ляет достаточный объем памяти в куче(специальной области памяти компьютера, зарезервированной для объектов иданных). ‘ Объектпродолжает «жить» благодаря ссылкамна него, которые могут храниться в переменных, коллекциях, свойствахилиполях другихобъектов. ‘ Наодин объект может существовать многоссылок —как было показанов главе 4, в которой ссылочные перемен- ныеlloydиlucindaуказывают на одинэкземплярElephant. ‘ Когда последняя ссылка на объектElephant перестает существовать, CLR помечает объект дляуничтожения входесборкимусора. ‘ Со временем CLRуничтожает объектElephant и ос- вобождает занимаемую им память, чтобы она могла использоваться для новых объектов, которые будут создаватьсяпозднее. Сейчас все эти факты будут исследованы более подробно. Мы напишем маленькие программы, демонстрирующие, как работаетсборка мусора. Но прежде чем начать эксперименты со сборкой мусора, не- обходимо сделать шаг назад. Ранее выузнали, что объекты «помечаются» для сборки мусора, но фактическоеудаление объекта может произойти в любой момент(а может не про- изойтиникогда!).Необходимо каким-тообразомузнать, когда объект уничтожается сборщиком мусора, а также принуди- тельно запустить процедурусборки мусора.С этого иначнем. Хлоп! О б ъ е к т El e p h a n t No 1 О б ъ е к т E le p h a n t # 2 lloyd О б ъ е к т E le p h a n t No 2 lucinda lucinda lloyd — У МЕНЯ...ОСТАЛОСЬ... —ОХ!— ОДНО... ПОСЛЕДНЕЕ... ДЕЛО... Диаграмма из главы 4. Мы соз- дали в куче два объекта Elephant, а затем удалили ссылку на один из объектов, чтобы пометить его для сборки мусора. Но что это означает в действитель- ности? Что выполняет сборку му сор а?
дальше 4 631 см ерть об ъекта В: Почему «клоны» похожи на робо- тов? Разве они не должны выглядеть как люди? О:Да, в своем комиксе мы придали кло- нам следующий вид: Потому что мы не хотели включать графи- ческиеизображенияуничтожаемых людей. В общем, расслабьтесь. Комиксы в этой главе помогают закрепить в вашем мозгу важнейшиеконцепцииC# и .NET. История — всего лишь инструмент, который помогает провестиряд полезных аналогий. В: У меня... есть вопросы. Что... как бы это выразиться... что такое Капитан Ве- ликолепный? О:КапитанВеликолепный — самый замечательныйобъектв мире, супергеройи защитник мирных гражданОбъектвиля, друг мелкихзверушек во всем мире. Аесли говорить более конкретно, КапитанВеликолепный является антропоморфным объ- ектом, вдохновленным одним из самых важных комиксов XXI века, посвященных смерти супергероя, аименно комиксом, вышедшим в2007 году,когда мыработалинад черновиком HeadFirstC#иискалихороший подход кобсуждению жизнии смерти объектов.Мы заметили поразительное сходство между формой объектов на диаграммах структуры кучи и щитом одного знаменитогоКапитана из комиксов... ТакродилсяКапитанВеликолепный.(Есливы не любитель комиксов,не огорчайтесь: вам не обязательно что-то знать о комиксах , чтобы понять материал этой главы.) Для принудительной сборки мусора используйте класс GC (осторожно!) .. N ET предоставляетв ваше распоряжение классGC,управляющийсборкой мусора. Мы будем исполь- зовать его статические методы, например метод GetTotalMemory, возвращающий значениеlong с при- близительным количеством байтов, выделенных в куче (предположительно): Возможно, вы подумали: «Почему приблизительным?Ичтозначит предположительно?Каксборщикмусора может незнать,сколькоименно памятивыделено?»В этойформулировкеотражаетсяодно из базовых правил сборкимусора: вы можетебезусловно, полностью положитьсяна сборку мусора,но существует много неизвестных иприближений. Вэтойглаве используются некоторыефункции GC: ‘ GC.GetTotalMemory возвращает приблизительное количествобайтов, предположительно вы- деленных в куче. ‘ GC.GetTotalAllocatedBytes возвращаетприблизительное количествобайтов, выделенныхсмомента запуска программы. ‘ GC.Collect заставляет сборщик мусора немедленно освободить все объекты, на которые нетни однойссылки. Поповоду этих методов необходимо сделать важное замечание: мы используем их для обучения и ис- следований,но использовать их следует только втом случае, есливы действительно хорошо знаете,что делаете. Еслине знаете — невызывайтеGC.Collectв реальном проекте.Сборщикмусора.NETявляется хорошо оптимизированным, тщательнооткалиброваннымпродуктоминженерноймысли. Какправило, когда заходитречь об определениитого, когда должны удаляться объекты, сборщик мусораумнее нас, инам стоит просто верить,что онвыполнитсвою работу. Обычный скромный объект. Капитан Великолепный, самый знаме- нитый объект в мире. часто Зада вае мые вопросы
632 глава 11 финализаторы объектов Финализатор объекта — последний шанс что-то сделать Иногда бывает нужно гарантировать, что некая операция (на- пример, освобождение неуправляемыхресурсов)будет вы- полнена дотого, какобъектбудет уничтожен сборщикоммусора. Специальный метод, называемый финализатором (finalizer), позволяет написать код, который всегда будет выполняться при уничтожении объекта. Код финализатора выполняется последним,что бы нипроизошло. Немного поэкспериментируемс финализаторами. Создайте но- воеконсольноеприложениеидобавьтекласссфинализатором: class EvilClone { public static int CloneCount = 0; public int CloneID { get; } = ++CloneCount; public EvilClone() => Console.WriteLine("Clone #{0} is wreaking havoc", CloneID); ~ EvilClone() { Console.WriteLine("Clone #{0} destroyed", CloneID); } } МетодMainсоздаетэкземплярыEvilClone, разыменовываетих(удаляет ссылки на них)иуничтожает объектыпри помощи сборщика мусора: class Program { static void Main(string[] args) { var stopwatch = System.Diagnostics.Stopwatch.StartNew(); var clones = new List<EvilClone>(); while (true) { switch (Console.ReadKey(true).KeyChar) { case 'a': clones.Add(new EvilClone()); break; case 'c': Console.WriteLine("Clearing list at time {0}", stopwatch.ElapsedMilliseconds); clones.Clear(); break; case 'g': Console.WriteLine("Collecting at time {0}", stopwatch.ElapsedMilliseconds); GC.Collect(); break; }; } } } Запуститеприложение инесколькоразнажмите a, чтобысоздать несколько объектов EvilClone и добавить их вList. Затем нажмите c, чтобы очистить List и удалить все ссылки на объектыEvilClone. Нажмитеc несколькораз — существует незначительная вероятность того, чтоCLR уничтожит некоторыеизразыменованныхобъектов,но ско- реевсего, онибудутуничтоженытолькотогда,когда вынажмете gдлявызоваGC.Collect. Вобщем случае никогда не следует пи- сать финализатор для объекта, который владеет толькоуправляемыми ресурса- ми. Всеми ресурсами, которые встреча- лись вам в книге до настоящего момен- та, управляла CLR. Однако вотдельных случаях программистам приходится обращаться к ресурсам Windows, не входящим в пространство имен .NET. Например, если в интернете вам встре- тится код, в котором перед объявлени- ем стоит директива [DllImport], возмож- но, вы используете неуправляемый ресурс. Некоторые из этих ресурсов, не находящихся под управлением .NET, при отсутствии необходимой «зачистки» могут оставить систему в нестабильном состоянии. Финализаторы нужны имен- но для таких ситуаций. Это финализатор (иногда называ- емый деструктором). Он объявля- ется как метод, имя которого начи- нается с тильды (~); финализатор не может иметь ни возвращаемого значения, ни параметров. Финали- затор объекта выполняется непо- сред ств енно п еред уни чтож ени ем объекта сборщиком мусора. Если оператор ++ находится перед именем переменной, то переменная инкрементируется до выполнения коман- ды. Как вы думаете, почему мы так поступили? Класс Stopwatch поможет полу- чить представление о том, на - сколько быстро выполняется сбор- ка мусора. Класс Stopwatch точно измеряет затраченное время; для этого он запускает отсчет времени и возвращает количество миллисекунд, прошедших с момен- та запуска. При нажатии клавиши ‘a’ приложение соз да ет но- вый экземпляр EvilClone и до- бавляет его в список clones. Нажатие ‘c’ приказывает приложению очистить список. При этом удаляются все ссылки на кло- нов, которые были созданы и добавлены в список. Нажатие ‘g’ приказывает CLR уничтожить все объекты, помеченные для сборки мусора.
дальше 4 633 см ерть об ъекта Финализатор объектавыполнятсяпослетого, как перестанутсуществоватьвсессылкина объект,но доуничтоженияобъекта сборщикоммусора.Сбор- камусорапроисходит толькопосле освобождения всехссылокна объект,ноона невсегдапроисходит сразу же после исчезновения последней ссылки. Предположим, имеется объект, инаэтот объект существует ссылка.СLR отправляет сборщикаму- соранаработу, тот проверяетваш объект.Таккак объектиспользуется(на негосуществует ссылка), сборщик мусора игнорируетего и двигается дальше.Объект продолжает находитьсяв памяти. Затем что-то происходит. Последний объект, содержащий ссылку на ваш объект, удаляет эту ссылку.Теперь вашобъект остаетсявпамятибез единой ссылки. К нему невозможно обратиться. Посути,это мертвыйобъект. Но тут возникает нюанс: сборкой мусора управ- ляет CLR, а не ваши объекты. Таким образом, если сборщик мусора не активизируетсяснова, скажем,черезнесколько секундилидажеминут, ваш объектпродолжит оставатьсявпамяти. Он неможет использоваться, но ещенебылуничто- жен сборщиком мусора. Ифинализатор объекта (еще) не можетбыть выполнен. Наконец, CLR снова отправляет сборщика му- сора на работу. Он проверяет ваш объект, об- наруживает, что ссылок на него не осталось, и выполняет финализатор... возможно, через несколько минут послеудаления или изменения последнейссылкина объект.После выполнения финализатора объект официально считается мертвым, исборщик мусорауничтожает его. Когда ИМЕННО выполняется финализатор? M y O b j e c t хлоп! .N E T позволяет порекомендовать провести сбор- ку мусора. В большинстве случаев этот метод лучше не использовать, потому что сборка мусора настроена так, чтобы онареагировала на многиеусловиявCLR,ипытатьсязапускать ее напрямую нежелательно. Но просто чтобы увидеть, как работает финализатор, можно ак- тивизировать сборкумусорасамостоятельно при помощиметода GC.Collect. Будьте внимательны. Метод не заставляет CLR немедленно провестисборкумусора. Онпросто говорит: «Выполни сборку мусора при первой возможности». Вы можете ПОРЕКОМЕНДОВАТЬ .NET выполнить сборку мусора Ваш объект в памяти. Другой объ ект ссылается на ваш объект. Ваш объект все еще находится в куче... ...но теперь не ос тал ось ни одной ссылки на не го. O t h er O b j e c t O t h e r O b j e c t Д ругой объект из м енил сво ю сс ы лк у. Куч а O t he r O b j e c t M y O bje c t Мы используем метод GC.Collect как учебный инструмент, который помогает вам понять, как работает сборка мусора. Использовать его за пределами учебных программ определенно не стоит (если только вы не понимаете, как ра- ботает сборка мусора в .NET, на более глубоком уровне, чем мы рассматриваем в этой книге.) Жизнь и смерть объекта на диаграмме 1. Ваш объектблагополучно существует в куче. Другой объект хранит ссылку на него, поэтому ваш объект остается живым. 2. Другой объект изменяет свою ссылку. Теперь не остается других объектов, ссылающихся на ваш о бъ ект. 3. CLR помечает объект для сборки мусора. 4. Через какое-то время сборщик мусора выполняет финализатор объекта и удаляет объект из кучи.
634 глава 11 финализированные объекты не могут обращаться к своим графам ГРОБ КАПИТАНА ПУСТ... НО ЧТО ЭТО? 6 e 6 1 6 d 6 5 7 3 7 0 6 1 6 3 6 5 2 0 5 1 7 b 0 d 0 a 5 b 5 3 6 5 7 2 6 9 6 1 6 c 6 9 7 a 6 1 6 2 6 c 6 5 5 d 7 0 7 5 6 2 6 c 6 9 6 3 2 0 6 3 6 c 6 1 7 3 7 3 2 0 4 d 7 3 6 7 7 b 0 d 0 a 7 0 7 5 6 2 6 c 6 9 6 3 2 0 7 3 7 4 7 2 6 9 6 e 6 7 2 0 6 1 3 b 7 0 7 5 6 2 6 c 6 9 6 3 2 0 7 3 7 4 7 2 6 9 6 e 6 7 2 0 6 2 3 b 7 0 7 5 6 2 6 c 6 9 6 3 2 0 7 3 7 4 7 2 6 9 6 e 6 7 2 0 6 3 3 b 7 0 7 5 6 2 6 c 6 9 6 3 2 0 6 9 6 e 7 4 2 0 6 9 3 b 0 d 0 a 7 0 7 5 6 2 6 c 6 9 6 3 2 0 7 6 6 f 6 9 6 4 2 0 5 3 6 8 6 f 7 7 2 8 2 9 7 b 4 d 6 5 7 3 7 3 6 1 6 7 6 5 4 2 6 f 7 8 2 e 5 3 6 8 6 f 7 7 2 8 6 3 2 e 5 3 7 5 6 2 7 3 7 4 7 2 6 9 6 e 6 7 2 8 3 1 2 c 3 2 2 9 КАПИТАН НАШЕЛ ВОЗМОЖНОСТЬ СЕРИАЛИ­ ЗОВАТЬ СВОЮ СУЩНОСТЬ ПЕРЕД СМЕРТЬЮ? ПОЗДНЕЕ В ПОХОРОННОМ БЮРО
дальше 4 635 см ерть об ъекта Финализаторы не могут зависеть от других объектов Принаписаниифинализаторанельзяполагатьсяна то, что он будет выполнен в какой-токонкретныймомент. ДажепривызовеGC.Collectвытолькорекомендуете вы- полнить сборщика мусора.Нет никакихгарантий, что этопроизойдет немедленно.Акогдаэто произойдет, вы не знаете, в какомпорядке будутуничтожатьсяобъекты. Чтоэтоозначаетвпрактическомсмысле?Представьте, что два объекта содержат ссылки другна друга. Если объект1будет уничтожен первым, то ссылка объек- та 2 указывает на несуществующий объект. Но если объект2уничтожается первым, то ссылка объекта 1 становится недействительной. Это означает, что вы неможетезависетьотссылокв финализаторевашего объ- екта. Аследовательно,в финализаторе не стоит делать ничего, что зависелобы отдействительности ссылок. Не используйте финализаторы для сериализации Сериализация — очень хороший пример того, что не стоитделать вфинализаторе.Есливаш объект содер- жит ссылки надругиеобъекты, сериализациязависит от нахождениявпамятивсехэтихобъектов...а также всех объектов, на которые они ссылаются, и объектов, на которые ссылаютсяэтиобъекты, ит. д.Таким образом, при попытке выполнения сериализации в процессе сборки мусора некоторые важные части программы могутоказатьсянедоступными,потомучтокакие-тообъ- екты былиуничтоженыдо выполненияфинализатора. Ксчастью, C#предоставляет хорошеерешение дляпо- добных проблем:IDisposable. Все операции,которые могутизменитьосновныеданные или зависят от нахож- дения впамятидругихобъектов,должнывыполняться вметоде Dispose,а нев финализаторе. Некоторыеразработчикирассматриваютфинализатор каксвоего рода «страховку» дляметода Dispose. И это выглядит логично — вы видели на примере объекта Clone, что фактреализации IDisposable еще не га- рантирует вызова метода Dispose. Однако вы должны действовать внимательно —если методDisposeзависит отнахожденияв куче другихобъектов, вызов Dispose из финализатора может создать проблемы. Лучшее решениевсех проблем такого рода — всегда исполь- зовать команду using каждыйраз, когда вы создаете объектIDisposable. Вначале два объекта содержат ссылки друг на друга. Если все остальные объекты в куче удаляют свои ссылки на объекты 1 и2, оба объекта будут помечены для сборки мусора. О б ъ е к т2 хлоп! С другой стороны, объект 2 может исчезнуть раньше объекта 1. Порядокуничтожения объектов заранее неизвестен. Если объект 1 уничтожается первым, то его данные будут недоступны в момент выполнения финализатора объекта 2 сре дой C LR. И именно по этой причине финализатор одного объекта не может зависеть от нахождения в куче любых других объектов. О б ъ е кт1 хлоп! О б ъ е к т2 О б ъ е к т1 Д р у г и е о б ъ е к т ы в к у ч е О бъ е к т 2 О б ъ е к т1 Помечен для сборки мусор а Помечен для сборки мусора
636 глава 11 Кто важнее: метод Dispose или финализатор? Dispose: Честноговоря,янемногоудивлен,чтоменясюда пригласили. Мнеказалось, что программистыуже пришли к согласию. Имею в виду, что я просто намного ценнее для разработчиковC#, чем ты. Будем откровенны: ты просто слабоват. Ты даже не можешь рассчитывать на то, что другиеобъ- екты еще существуют на момент твоего вызова. В ыхо дит, ты недо ста то чно над ежен. Интерфейс существует именно потому, что я ва - жен. В нем вообще нет других методов! Здесь тыправ, программист должензнать, когдаон захочетвоспользоваться моимиуслугами,и либо вызвать меня напрямую, либо воспользоваться командой для using.Но они всегда знают, когда я буду выполняться, и используют меня, чтобы сделать все необходимое для наведения порядка после освобожденияобъектов. Я мощен, надежен и прост в использовании. Тройная польза. А ты? Никто не знает, когда ты будешь выполняться или в каком состоянии окажется приложение после того, как ты наконец-то соизволишь показаться . Ты думаешь, что ты большая шишка, потому что всегдавыполняешьсясосборщиком мусора, но по крайнеймереямогузависеть от другихобъектов. Финализатор: Прошупрощения?Этосильно сказано. Я «слабоват»? Чтож, янехотелввязываться вдискуссию, ноеслиуж мыопустились до такого...преждевсегомне не нужен интерфейс. Давай признаем, что без интерфейса IDisposable...тыпростообычныйбесполезныйметод. Конечно, конечно... продолжай убеждать себя в этом. И что произойдет,есликто-нибудь забудет использовать команду using при создании экземп- ляра?Раз— итебя вообщенет. Ноеслинужно сделать что-то в самый последний момент, непосредственно перед уничтожением объекта сборщиком мусора, без меня необойтись. Ямогу освобождать сетевые ресурсыи дескрип- торыWindows... и вообще все, что необходимо освободить, чтобы невозникли проблемы. Ямогу гарантировать, что объекты будут уничтожаться более корректно, и это не игрушки. Да, это верно — но я выполняюсь всегда. Тебе не- обходим кто-то другой,кто тебя вызовет. А мнене нужен никто иничего! что произошло с капитаном? Дескрипторы — то, что используют ваши программы при прямом взаимо- действии с системой Windows. Так как среда .NET о них ничего не знает, она не сможет освободить их за вас. Беседа у камина
дальше 4 637 В:Может ли финализатор использовать все поля и методы объекта? О: Да.Хотяметод-финализатор неможет получать параметры,вы можетеиспользовать любыеего поля вобъекте, напрямую или при помощиthis, но будьтевнимательны, потому что если эти поля ссылаются на другие объекты, то другиеобъекты могут быть уже уничтожены сборщиком мусора. Такимобразом, ваш финализатор может вызывать другиеметоды илисвойстваобъекта...приусловии, что эти методы и свойства не зависят отдругих объектов. В:Что происходит при выдаче исключений вфинализаторе? О: Хорошийвопрос. Исключениявфинализаторахработают точно так же,как влюбойдругой точкевашего кода. Попробуйтезаменить финализатор EvilClone другим, в котором выдается исключение: ~ EvilClone() => throw new Exception(); Затемсновазапустите приложение, создайте несколько экземпля- ровEvilClone, очистите список и запустите сборщик мусора. Ваше прил ожение пре рветс я в фи нали зат оре, как и при возн икнове нии любых других исключений. (Спойлер: в следующей главе вы на- учитесь перехватывать исключения, т . е. обнаруживать факт их возн икно вения и выпо лнят ь код обр абот ки.) В:С какой частотой сборщикмусора выполняется автоматически? О:Простой ответ: неизвестно. Сборщик мусора не выполняется с какой-то прогнозируемой периодичностью и не находится под контролем разработчика. Вы можетебыть уверены только в том, чтоон будетвыполненпринормальном выходе из программы.Даже при вызовеGC.Collect (чегообычноделать нерекомендуется) вы только рекомендуетеCLR выполнить сборку мусора. В: Через какое время после вызова GC.Collect начнется сборка мусора? О:ПривызовеGC.Collect вы сообщаете .NET, что сборка мусора должнабыть выполнена как можно скорее. Обычно этопроисходит тогда, когда .NET сделает то, чем занимаетсяв настоящий момент. Скореевсего, этопроизойдетдостаточноскоро,но точно управлять началом сборки мусора вы не можете. В: Если какой-то код должен быть выполнен абсолютно точно, я помещаю его в финализатор? О:Нет. Может оказаться, что финализатор выполнен небудет. Выполнение финализаторов при сборке мусора может быть за- блокировано. Или может завершиться весь процесс. Если только вы не освобождаете неуправляемыересурсы в своем коде, почти всегда лучше использовать IDisposable и команды using. КАПИТАН ВЕЛИКОЛЕПНЫЙ КАКИМ­ТО ОБРАЗОМСОХРАНИЛ ВСЮСВОЮ СУЩНОСТЬВ ЭТОЙ ЗАПИСКЕ. ...НОКАК ТЕПЕРЬ ЕГО ВОССТАНО­ ВИТЬ? часто Задава ем ые вопросы
638 глава 11 ТЕМ ВРЕМЕНЕМ НА УЛИЦАХ ОБЪЕКТВИЛЯ... КАПИТАН ВЕЛИКОЛЕПНЫЙ... ОН ВЕРНУЛСЯ! С НИМ ЧТО­ТО НЕ ТАК. ОН НЕ ПОХОЖ НА СЕБЯ ПРЕЖНЕГО... И СО СВЕРХСИЛАМИ ТВОРИТСЯ ЧТО­ТО СТРАННОЕ . ПОЗЖЕ... КАПИТАНУ ВЕЛИКОЛЕПНОМУ ПОНАДОБИЛОСЬ СТОЛЬКО ВРЕМЕНИ, ЧТОБЫ ДОБРАТЬСЯ СЮДА, ЧТО ПУШИСТИК САМ СЛЕЗ С ДЕРЕВА. МЯУ ! ЕЩЕ ПОЗЖЕ... УФ... ПЫХ... ФУ... МНЕ НАДО ПЕРЕВЕСТИ ДУХ. ЧТО ПРОИЗОШЛО? ПО­ ЧЕМУ СИЛЫ КАПИТАНА ВЕЛИКОЛЕПНОГО РАБО­ ТАЮТ НЕ ТАК, КАК ПРЕ ­ ЖДЕ? ЭТО КАТАСТРОФА? ТАЙНОЕ УБЕЖИЩЕ КАПИТАНА ВЕЛИКОЛЕПНОГО СОВЕРШЕННО СЕКРЕТНО
дальше 4 639 см ерть об ъекта Структура похожа на объект... Мыговорилиокуче, вкоторойсуществуютвашиобъекты.Тем не менее этоне един- ственнаячасть памяти,вкотороймогутразмещаться объекты. Одиниз типов.NET, неупоминавшийся до настоящего момента, — структуры — поможетнамразобрать- ся с другими аспектами жизнии смерти в C#. Структуры (ключевое слово struct) очень похожина объекты. У них,как иу объектов,могутбыть поля исвойства.Их дажеможно передавать методам,которые получаютпараметр объектного типа: public struct AlmostSuperhero : IDisposable { private bool superStrength; public int SuperSpeed { get; private set; } public void RemoveVillain(Villain villain) { Console.WriteLine("OK, {0}, surrender now!", villain.Name); if (villain.Surrendered) villain.GoToJail(); el se villain.StartEpicBattle(); } public void Dispose() => Console.WriteLine("Nooooooo!"); } . . . но не является объектом Однако структурынеявляютсяобъектами.Онимогут содержатьметоды иполя, но не могут иметьфинализаторы. Они также немогутнаследо- вать от классов и других структур, и другие классы или структуры не могут наследовать от них: оператор :может использоваться вобъявле- нииструктуры, но за ним могут следовать только одинилинесколько интер фейс о в. S u p e r H e r o struct Структуры не могут наследо- вать от других объектов. Автономный объект можно смоделировать в виде структуры, но структуры плохо подходят для сложных иерархий с наследова- нием. Мощь объектов свя- зана с их способно- стью моделировать поведение сущностей реального мира по- средством наследова- ния и полиморфизма. Структуры лучше всего подходят для хранения данных, но отсутствие наследова- ния и ссылок может стать серьезным огра- ничением. ... и определять методы. Структуры могут реализовать интер- фейсы, но не могут расширять другие классы. Кроме того, структуры не мо- гут расширяться. Структуры могут содер- жать поля и свойства... Структуры даже могут реализовать интерфейсы (на- пример, IDisposable). Все структуры расширя- ют класс System.ValueType, который, в свою очередь, расширяетSystem.Object. Вот почему каждая структура под- держивает метод ToString — он получает его от Object. Но это единственное наследование, разрешенное для структур.
640 глава 11 L i s t <d o u b l e > Значения копируются, ссылки присваиваются Выуже видели, какую важнуюроль ссылкииграют вобластисборкимусора, — стоит вам присвоить новое значение вместопоследней ссылки наобъект, и объект помечаетсядлясборкимусора.Номы также знаем, что этиправила не имеютсмысла для значений.Есливы хотителучшепонять,как объекты изначения живут иумирают в памятиCLR, необходимо повнимательнее присмотреться к значениям и ссылкам: чемонипохожии,чтоважнее,— чем ониотличаются. Выуже примерно представляете,чем одни типы отличаются отдругих. С одной стороны, существуют типы значений, такие как int, bool иdecimal.С другой стороны, существуют объекты, такие как List, Stream иException. Иработаютонисовсем неодинаково, верно? Когда вы используете оператор = для того, чтобы присвоить одну переменную типа значения другой, создается копия значения, ив дальнейшем две переменные никак несвязаны другс другом. С другой стороны,когда выиспользуетеоператор =со ссылками,обе ссылкибудутуказывать на одинобъект. Объявления переменных и присваивания работают для типов значений и объектных типов одинаково: 1 Но когда вы начинаете присваивать значения, проявляютсяразличия между ними. Для всехтипов значений используется копирование. В сле- дующем примеревам вседолжно быть знакомо: 2 После выпол- нения этой строки пере- менная copy ссылается на тот же объект, что и переменная temps. Эта строка копирует значение, хранящееся в переменной fifteenMore, в переменную howMany и увеличи- вает его на 15. Изменение переменной fifteenMore не влияет на howMany, и наоборот. При вызове copy.Add метод до- бавил новую температуру к объекту, на который указыва- ют как copy, так и temps. int и bool являются типами значений, List и Exception — объектные типы. int howMany = 25; bool Scary = true; List<double> temps = new List<double>(); throw new NotImplementedException(); int fifteenMore = howMany; fifteenMore += 15; Console.WriteLine("howMany has {0}, fifteenMore has {1}", howMany, fifteenMore); Результат ясно показывает, что переменныеfifteenMore и howMany не связаны: howMany has 25, fifteenMore has 40 Нокаквыужезнаете, приработес объектамиприсваиваютсяссылки,а не значения: 3 temps.Add(56.5D); temps.Add(27.4D); List<double> copy = temps; copy.Add(62.9D); Обе ссылки ука- зывают на один объект. Такимобразом, изменениеList означает, чтообновлениебудет видимым пообеим ссылкам, так как они обеуказываютна одинобъектList. Чтобыубедиться вэтом,выведите следую- щую стр о ку: Co nsole .Writ eLine ("tem ps ha s {0} , cop y has {1}" , tem ps.Co unt() , cop y.Cou nt()) ; temps has 3, copy has 3 Следующий результат демонстрирует, что copy и temps действительноуказываютна одини тот же объект: значения и ссылки copy temps
дальше 4 641 см ерть об ъекта Структуры относятся к типам значений; объекты относятся к ссылочным типам Присмотримсяповнимательнеектому,какработаютструктуры, чтобывыхотябыв общихчертахпредстав- ляли, вкакихслучаяхстоит использовать структурувместообъекта.Присозданииструктурывысоздаете тип значения. Это означает, что, присваивая одну структурную переменную другой, вы создаете новую копию структурывновойпеременной.Итак, хотя структурапохожанаобъект, она не работает какобъект. Создайте структуру с именем Dog. Нижеприведена простаяструктурадляхранения информациио собаке. Она выглядит какобъ- ект,но объектом не является. Добавьте еев новое консольноеприложение. 1 public struct Dog { public string Name { get; set; } public string Breed { get; set; } public Dog(string name, string breed) { this.Name = name; this.Breed = breed; } public void Speak() { Console.WriteLine("My name is {0} and I'm a {1}." , Name, Breed); } } Создайте класс с именем Canine. Создайтеточную копию структурыDog,но заменитеstructна class,азатем заменитеDogна Canine. Не забудьте переименовать конструктор Dog. Теперь у вас имеется классCanine, с кото - рымможноэкспериментировать;онпочтиэквивалентенструктуреDog. 2 Добавьте метод Main, в котором создаются копии данных Dog и Canine. Код метода Main выглядит так: Canine spot = new Canine("Spot", "pug"); Canine bob = spot; bob.Name = "Spike"; bob.Breed = "beagle"; spot.Speak(); Dog jake = new Dog("Jake", "poodle"); Dog betty = jake; betty.Name = "Betty"; betty.Breed = "pit bull"; jake.Speak(); 3 Прежде чем запускать программу... Напишите, что, по-вашемумнению, будетвыведенонаконсоль при выполненииследующего кода: 4 С д е л а й т е э т о ! M INI Возьмив руку карандаш
642 глава 11 My name is Spike and I’m a beagle. My name is Jake and I’m a poodle. Вот что произошло... Ссылки bob и spotуказывают на один и тот же объект, поэтомупо обеим ссылкам изменяются одни ите же поля ивызывается одини тот жеметодSpeak.Структу- ры работают не так. При создании betty была создана новая копия данных из jake. Две структуры полностью независимы друг от друга. Dog jake = new Dog("Jake", "poodle"); Dog betty = jake; betty.Name = "Betty"; betty.Breed = "pit bull"; jake.Speak(); Canine spot = new Canine("Spot", "pug"); Canine bob = spot; bob.Name = "Spike"; bob.Breed = "beagle"; spot.Speak(); jake 4 5 1 2 О б ъ е кт C a n i n e 1 О б ъ е кт C a n i n e 2 3 6 Был создан новый объ е кт, на кото- рый указывает ссылка spot. Новая ссылочная переменная bob была создана, но в куче не появился новый объект — пере- менная bob указывает на тот же объект, что и spot. Sp ot pug О б ъ е кт C a n i n e 3 Spi ke beag le Spo t pug Так как spot и bob указывают на один объ- ект, spot.Speak и bob.Speak вызывают один и тот же метод, и в обоих случаях будет получен один и тот же вывод со "Spike" и "beagle". Jake poodle jake Jake poodle betty Jake poodle betty Betty pit bull jake Jake poodle 4 5 6 Присваивая одну структуру другой, вы создаете новую КОПИЮ данных, содер- жащихся в структуре. Дело в том, что структура относится к ТИПУ ЗНАЧЕНИЯ (а не к объектному/ссылочному типу). Когда вы создаете новую структуру, это очень похо- же на создание объекта — вы получаете переменную, кото- рая может использоваться для обращения к полям и методам. И это очень серь езное различие. Добавляя пере- менную betty, вы создае- те новое значение. Так как вы создали новую копию данных, изменение полей betty не отразилось на jake. Как выдумаете,чтобудет выведенона консоль? стек и куча s p o t bob bob s p o t s p o t MI N I Возьми в руку карандаш Решение
дальше 4 643 см ерть об ъекта Стек и куча: подробнее о памяти Давайтебыстровспомним, чем структураотличаетсяотобъекта. Выуже виде- ли, что новую копиюструктурыможносоздатьпростымприсваиванием,тогда какс объектами этоневозможно. Но что при этом происходитза кулисами? CLRраспределяет данные между двумяобластями памяти:кучей и стеком. Вы ужезнаете, что объекты существуютв куче. CLR такжерезервирует другую часть памяти,называемуюстеком;в ней сохраняютсялокальныепеременные, объявленные в методе, и параметры,передаваемые этимметодам.Стекможно рассматривать какнаборячеек,вкоторыхмогут сохранятьсязначения. При вызове метода CLR добавляетновыеячейкинавершинустека. Привозврате управления ячейки вызванного метода удаляются. Код Пример кода, который вам может встретиться в программе. Стек Здесь хранятся структуры и локальные переменные. Dog spot Dog spot Dog spot Canine spot = new Canine("Spot", "pug"); Dog jake = new Dog("Jake", "poodle"); Canine spot = new Canine("Spot", "pug"); Dog jake = new Dog("Jake", "poodle"); Dog betty = jake; public SpeakThreeTimes(Dog dog) { int i; for(i=0;i<5;i++) dog.Speak(); } Dog betty Dog betty Dog jake Dog jake Dog jake Dog myDog int i Dog dog Canine spot = new Canine("Spot", "pug"); Dog jake = new Dog("Jake", "poodle"); Dog betty = jake; SpeakThreeTimes(jake); Так будет выглядеть стек после выполнения этих двух строк. При создании новой структуры — или любой другой переменной, относящейся к типу значения, — в стек добавляется новая «ячейка». Она содержит копию значения соответствующего типа. Помните: во вре- мя выполнения ва- шей программы CLR активно управляет памятью, работает с кучей и собирает мусор. При вызове методаCLR по- мещает его локальные пере- менные на вершину стека. В данном случае код вызы- вает м етод SpeakT hr eeTimes. Метод получает один пара- метр (dog) и создает одну переменную (i); CLR сохра- няет их в стеке. При возврате управления из методаCLR извлекает i и dog из стека — так прохо- дитжизнь и смерть значе- ний в CLR. Засценой
644 глава 11 После того как структура будет упакована, суще- ствуют две ко- пии данных: копия в стеке и копия, упакованная в куче. Dog sid (упакован) Теория — это, конечно, здорово, но я уверен, что у всех этих разговоров о стеке, куче, значениях и ссылках есть и практические причины. Важно понимать, чем структура, копируемая по значению, отличается от объекта, копируемого по ссылке. Вотдельныхслучаяхтребуетсянаписать метод, которыйможет получать типзна- ченияилиссылочный тип, — например, метод,способныйработать со структурой Dogили с объектомCanine. Если вы окажетесь в такой ситуации, используйте ключевое слово object: public void WalkDogOrCanine(object getsWalked) { ... } Если передать такому методу структуру, эта структура будет упакована в спе- циальный объект-«обертку», которая позволяетей существовать в куче. Пока обертка находится в куче, вы мало что сможете сделать со структурой. Необ- ходимо «распаковать» обертку, чтобыработать со структурой. К счастью, все это происходит автоматически при присваивании объекту типа значения или передачетипа значенияметоду,который ожидаетполучить объект. Sid hu sky Таквыглядят стекикуча после созданияобъектнойпеременной и присваиванияей структурыDog. Dog sid = new Dog("Sid ", "husky"); WalkDogOrCanine(sid); 1 Dog sid (boxed) Sid h usky Есливыхотитераспаковатьобъект, дляэтогодостаточнопреобразовать его кправильномутипу;объектбудет распакован автоматически. Ключе- воеслово is нормальноработаетсо структурами,но будьтеосторожны, потому что ключевоеслово as не работает с типами значений. if (getsWalked is Dog doggo) doggo.Speak(); 2 Dog sid Object getsWalked Dog sid Object getsWalked Dog doggo Также можно восполь- зоваться ключевым словом «is», чтобы узнать, является объ- ект структурой или любым другим типом значения, упакованным и помещенным в кучу. близкое знакомство со стеком После выполнения этой строки по- является третья копия данных в новой структуре с именем doggo, которой назнача- ется отдельная ячейка в стеке. The WalkDogOrCanine получает ссылку на объект, поэто- му структура Dog была упакована до ее передачи. После при- ведения ее обратно к типу Dog происхо- дит ее распаковка. g e t s W a l k e d g e t s W a l k e d
дальше 4 645 см ерть об ъекта При вызове метод ищет свои аргументы в стеке. Стекиграет важную роль вуправлении данными приложения средой CLR. Приэтоммыприняли как нечто само собойразумеющееся, что мы можем написать метод, вызывающий другой метод, который, в свою очередь, вызывает другой метод. Более того, метод даже может вызывать самого себя(это называетсярекурсией).Именно стек предоставляетнашимпрограммам такую возможность. public double FeedDog(Canine dogToFeed, Bowl dogBowl) { double eaten = Eat(dogToFeed.MealSize, dogBowl); return eaten + .05D; // Небольшая часть всегда теряется. } public void Eat(double mealSize, Bowl dogBowl) { dogBowl.Capacity - = m ealSize; CheckBowl(dogBowl.Capacity); } public void CheckBowl(double capacity) { if (capacity < 12.5D) { string message = "My bowl's almost empty!"; Console.WriteLine(message); } } Ниже приведены три метода из программы, моделирую- щейповедениесобаки.Метод FeedDog вызывает метод Eat, rn вызывает метод CheckBowl. Dog myDog М етод Feed Do g пол уча- ет два параметра, ссыл - ку на Canine и ссылку на Bowl. Таким образом, при вызове в стеке оказываются два пере- данных аргумента. FeedDog передает двааргумента методу Eat, поэтому эти аргу- менты также должны быть занесены в стек. Запомните терминоло- гию: параметр задает значение, необходимое методу; аргумент — это фактическое значе- ние или ссылка, передава- емые методу при вызове. Такбудет выглядеть стек при вы- зовеEatизFeedDog; методвызы- вает CheckBowl, который, в свою очередь, вызывает Console. Wri teL i ne. Когда вызовов стано- вится все больше, а одни методы начинают вызывать другиеме- тоды, которые, в свою очередь, вызывают еще какие-то методы, размер стек а ув ели чи вается. Когда метод WriteLine зав ерш ается, ег о ар- гументы извлекаются из стека. После этого метод Eat продолжает выполнение так, слов- но управление никуда не перед ав ало сь. Перейдите к проекту Unity и наведите указатель мыши на Vector3 — это структура. Сборка мусора может серьезно понизить быстродействие приложения, а большое коли- чество объектов в игре порождает лишние сборки мусо- ра иснижаетчастоту кадров. В играх часто используется МНОЖЕСТВО векторов. Преобразование их в структуры означает, что их данные будут храниться в стеке, так что создание даже миллионов векторов не вызоветдополни- тельной сборки мусора, замедляющие игру. Так как Vector3 в Unity явля- ется структурой, создание нескольких векторов не вы- зовет дополнительной сборки мусора. Bowl dogBowl Bowl dogBowl Bowl dogBowl Bowl dogBowl Bo wl dog Bowl Bowl dogBowl Bowl dogBowl Ca nine dogTo Feed Canine dogToF eed Can ine dogTo Feed Canine dogToFeed mealSize value mealSize value capa cit y v alue ca pac ity val ue c apac ity va lue string message п а р а м е т р ы , д о б а в л е н н ы е в с т е к Засценой
646 глава 11 параметры out и ref Параметры out и возвращение нескольких значений методом Разужречь зашла о параметрахи аргументах,существуютидругиеспособы передачи значений в программу и из нее. Все они требуют включения модификаторов в объ- явления методов. В одном из самых распространенных способов модификатор out используетсядлязаданиявыходногопараметра.Модификатор outвамуже знаком— он используетсякаждыйраз, когдавы вызываете методint.TryParse.Крометого, выможете использовать модификатор out в своих собственных методах. Создайте консольное приложение и добавьте вформу объявление пустого метода. Обратитевниманиена модификаторы out уобоих параметров: Взгляните внимательно на описание двух ошибок: ‘ Выходному параметру‘half’должнобыть присвоенозначениеперед возвратомуправленияизтекущегометода. ‘ Выходномупараметру‘twice’должнобыть присвоенозначениепередвозвратомуправленияиз текущегометода. Каждыйраз, когда выиспользуетепараметр out, всегданеобходимо присвоить емузначение довозврата из метода— поаналогии стем,какметод, возвращающий значение, всегдадолжен содержатькомандуreturn. Полный код приложения: public static int ReturnThreeValues(int value, out double half, out int twice) { half = value / 2f; twice = value * 2; return value + 1; } static void Main(string[] args) { Console.Write("Enter a number: "); if (int.TryParse(Console.ReadLine(), out int input)) { var output1 = ReturnThreeValues(input, out double output2, out int output3); Console.WriteLine("Outputs: plus one = {0}, half = {1:F}, twice = {2}", output1, output2, output3); } } При запуске приложения будет получен следующийрезультат: Enter a number: 17 Outputs: plus one = 18, half = 8.50, twice = 34 Метод мо- жет вернуть н еско лько значений при помощи параметров out . Знакомый код, который уже использовался в книге; моди- фикатор out используется с int.TryParse для преобразо- вания строки в int. Значения всех пара- метров с модифи- катором out должны быть присвоены до выхода из метода. Также модификатор out используется при вызове нового метода. Сделайте э то!
дальше 4 647 см ерть об ъекта Передача по ссылке с модификатором ref Каквыуже неоднократно видели, каждыйраз,когда вы передаете методу int, double, struct илилюбой другойтипзначения, методупередаетсякопияэтогозначения. Утакого механизма передачисуществует специальноеназвание: передача по значению.Это означает,что значение аргумента полностью копи- руетсяна сторонувызова. Однако существует ещеодин способпередачиаргументов методам—такназываемаяпередача поссылке. Ключевоеслово refпозволяет методуработать напрямую с переданным емуаргументом. Какивслучае с модификатором out, вы должны использовать ref при объявлении метода, а также при его вызове. Приэтомневажно,относится аргумент к типу значения или к ссылочному типу, — любая переменная, передаваемая впараметреref метода,будетнапрямую изменяться этим методом. Чтобы понять, как работает этот механизм, создайте новое консольное приложение с классомGuy и следующими методами: c lass Guy { public string Name { get; set; } public int Age { get; set; } pu blic overr ide s tring ToSt ring( ) => $"a { Age}- year- old n amed {Name }"; } c lass Progr am { st atic void Modif yAnIn tAndG uy(re f int valu eRef, ref Guy g uyRef ) { val ueRef += 1 0; guy Ref.N ame = "Bob "; guy Ref.A ge = 37; } st atic void Main( strin g[] a rgs) { vari=1; var guy =new Guy(){Name= "Joe",Age=26}; Con sole. Write Line( "i is {0} and g uy is {1}" , i, guy); Mod ifyAn IntAn dGuy( ref i , ref guy) ; Console.WriteLine("Now i is {0} and guy is {1}", i, guy); } } Запустите приложение — оно выводит на консоль следующий результат: iis1andguyisMynameisJoe Nowiis11andguyisMynameisBob Типы значений содержат метод TryParse с параметрами out Вы пользовались методом int TryParse для преобразования строк в значения int. Сходные функции поддерживаются другими типами значений: double.TryParse пытается преобразовать строки в значения double, bool.TryParse делает то же самое с логическими значениями и т. д . для decimal. TryParse, float.TryParse, long.TryParse, byte.TryParse и других. Также вспомните, что в главе 10 мы использовали команду switch для преобразования строки "Spades" в значение из перечисления Suits. Spades. Статический метод Enum.TryParse делает то же самое, если не считать перечисления. Во внутренней реализации аргумент out почти не от- личается от аргумента ref, кроме того что ему не обязательно присваивать значение перед вызовом метода и оно должно присваиваться перед возвратом из метода. Вторая строка отличается от первой, потому что ModifyAnIntAndGuy изменя- ет ссылки на переменные в методе Main. Когда этот метод задает значения valueRef и guyRef, на самом деле он изменяет значе- ния переменных в том методе, из которого он был вызван. Когда метод Main вызывает ModifyAnIntAndGuy, пере- менные i и guy передаются по ссылке. Метод работает с ними как с любыми дру- гими переменными. Но по- скольку они передавались по ссылке, метод обновляет исходные значения, а не их копии в стеке. При заверше- нии метода переменные i и guy в методе Main обнов- ляются соответствующим образом.
648 глава 11 необязательные аргументы Достаточночастовашиметоды снова иснова вызываютсяс одними итемижеаргументами,но методу все равно нужен параметр, потому что в отдельных случаях значение все же изменяется. Было бы по- лезно иметь возможность задать значениепо умолчанию,чтобы аргументможнобыло задавать только привызовеметода с новым значением. Необязательныепараметрынужны именно длятаких случаев. Чтобы задатьнеобязательныйпараметр вобъявлении метода, поставьтепослепараметра знак=иукажитезначениепоумолчанию.Необязатель- ных параметровможетбыть сколькоугодно,но все они должны следовать послеобязательных. Пример метода с использованием необязательных параметров: static void CheckTemperature(double temp, double tooHigh = 99.5, double tooLow = 96.5) { if (temp < tooHigh && temp > tooLow) Console.WriteLine("{0} degrees F - feeling good!", temp); els e Console.WriteLine("Uh-oh {0} degrees F -- better see a doctor!", temp); } Методимеет два необязательныхпараметра:tooHighимеет значение 99.5, а tooLow —значение поумол- чанию 96.5. ПривызовеCheckTemperatureс одним аргументом значения поумолчанию используются как для tooHigh, таки для tooLow. Привызовес двумя аргументами второй аргументстановитсязначе- нием tooHigh,тогда какдля tooLow сохраняется значение поумолчанию. Если при вызовепередаются всетри аргумента, то для всехтрех параметровзначения задаютсяявно. Есливы хотитеиспользовать некоторые(но не все) значения поумолчанию, используйте именован- ныеаргументыдляпередачизначений только техпараметров,которые вамнужны. Все,что для этого потребуется, — присвоить каждому параметруимя, за которым следуетдвоеточие. Если вы используете болееодного именованного аргумента,разделитеихзапятыми,как илюбые другиеаргументы. Добавьте метод CheckTemperature в консольное приложение, затем добавьте следующий метод Main: stat ic vo id Ma in(st ring[ ] arg s) { // З начен ия по дходя т для сред него челов ека Chec kTemp eratu re(10 1.3); // Т емпер атура соба ки от 100. 5 до 102.5 по Ф аренге йту Chec kTemp eratu re(10 1.3, 102.5 , 100 .5); // Т емпер атура Боба всег да ни же об ычной , зад аем to oLow значе ние 9 5.5 Chec kTemp eratu re(96 .2, t ooLow : 95. 5); } Программа выводит следующийрезультатсразнымизначенияминеобязательных параметров: Uh-oh 101.3 degrees F -- better see a doctor! 101.3 degrees F - feeling good! 96.2 degrees F - feeling good! Необязательные параметры и значения по умолчанию Если вы хо- тите, чтобы у метода были значения по умолчанию, используйте необязатель- ные параме- тры и име- нованные аргументы. Необязательные параметры имеют значения по умолча- нию, заданные в объявлении.
дальше 4 649 см ерть об ъекта Ссылка null не указывает ни на какой объект Когда вы создаетеновую ссылку, но не присваиваете ей значение, так что она ни на что не указывает, уэтой ссылки есть значение. Изначально ссылка содержитnull — собственно, это иозначает, что она нина что неуказывает. Поэкспериментируемс null-ссылками. Создайте новое консольное приложение и добавьте класс Guy, который использовался для экспериментовс ключевым словом ref. Добавьте следующийкод, который создаетновыйобъектGuy, нонеинициализируетегосвойствоName: static void Main(string[] args) { Guy guy; guy=newGuy(){Age=25}; Console.WriteLine("guy.Name is {0} letters long", guy.Name.Length); } Установитеточкупрерываниявпоследнейстроке метода Main изапустите отладкуприложения. Придостижении точки прерывания наведитеуказатель мыши на объектguy,чтобы просмо- треть значенияего свойств: Продолжите выполнение кода. Метод Console.WriteLine пытается обратитьсяксвойствуLength объектаString, на которыйссылается свойствоguy.Name,и программа выдает исключение: 1 2 3 4 Строка является ссылочным типом. Так как ей не было присвоено значение при инициализации объекта Guy, она сохраняет свое значение по умолчанию: null. Когда среда CLR вы- даетисключение Nu ll Ref er enceExcep t io n (которое разработчики ча- сто обозначают сокраще- нием NRE), она сообщает вам о том, что программа попыталась обратиться к компоненту объекта, но ссылка, которая для этого использовалась, содер- жала null. Разработчики стараются предотвратить исключения null-ссылок. А как бы вы подошли к предотвращению исключений null-ссылок в программах? Мозговой штурм Сдел а йте эт о!
650 глава 11 ссылочные типы, не допускающие null Ссылочные типы, не допускающие null, помогут избежать NRE Самыйпростойспособизбежать исключенийnull-ссылок(илиNRE)—спроектировать код так, чтобы ссылкинемоглисодержатьnull.Ксчастью, компиляторC#предоставляет очень полезныйинструмент длярешенияпроблемс null.Добавьте следующийфрагментвначалоклассаGuy(онможетрасполагаться как до объявленияпространства имен,так ипосленего): #nullable enable Строка, начинающаясяс#, являетсядирективой, т.е . средствомпередачи конкретныхпараметров компи- лятору.В данномслучае она приказывает компиляторурассматривать всессылки как ссылочныетипы, не допускающиеnull.Сразуже последобавлениядирективыVisualStudioподчеркиваетсвойствоName преду- преждающей красной линией.Наведитеуказательмышина свойство, чтобыпросмотреть предупреждение: КомпиляторC#проделалнечто очень интересное:он воспользовалсяанализом путей передачиуправления, чтобыопределить, возможен липуть, накотором свойствоNameбудет содержатьnull.Этобудет означать, что кодтеоретическиможетвыдать NRE. Чтобы избавитьсяотпредупреждения,можно принудительно объявить свойствоNameсо ссылочным типом, допускающим null.Для этого следуетдобавить символ? послетипа: public string? Name { get; set; } Впрочем, хотясообщение об ошибкепропадет,возможность выдачиисключений остается. Используйте инкапсуляцию для того, чтобы свойство было заведомо отлично от null Вглаве5 выузналио том, как при помощи инкапсуляциипредотвратить присваивание некорректных значений компонентам класса. Объявите свойствоName приватным, а затем добавьте конструктор, вкотором емубудет присваиваться значение: class Guy { public string Name { get; private set; } public int Age { get; private set; } public override string ToString() => $"a {Age}-year-old named {Name}"; public Guy(int age, string name) { Age = age; Name = name; } } Инкапсуляция свойстваName предотвращает возможность присваивания ему null, и предупреждение исчезает. Объявление свойства Name с типом, допускающим null, подавляет предупреждение, но не решает проблему. Объявление set-метода свойства Name приват- ным и добавление конструктора гарантирует, что свойству всегда будет присвоено значение. Таким образом, оно никогда не будет равно null, поэтому предупреждение компилятора исчезает.
дальше 4 651 см ерть об ъекта Оператор объединения с null ?? Иногда без обработки null просто необойтись. Например, в главе10 выузналио чтении данных из строк с использованием класса StringReader. Создайте новое консольное при- ложение и добавьте следующий код: #nullable enable class Program { static void Main(string[] args) { using (var stringReader = new StringReader("")) { var nextLine = stringReader.ReadLine(); Console.WriteLine("Line length is: {0}", nextLine.Length); } } } Запустите код — возникает исключение NRE.Чтос этим можносделать? ?? проверяет null и возвращает альтернативу Один из способов предотвратить обращение по ссылке null (разыменование ссылки) — использование оператора объединения с null ?? для проверки выражения, которое тео- ретически можетбыть равно null (в данном случае stringReader.ReadLine), и возвращения альтернативного значения,если оно равно null.Изменитепервую строкублока usingи до- бавьте?? String.Empty в конец строки: var nextLine = stringReader.ReadLine() ?? String.Empty; Кактольковы добавите оператор, предупреждение исчезает. Дело в том, что оператор объединения с null приказывает компиляторуC# выполнить строку stringReader.ReadLine; и использовать возвращенное значение, если оно отлично от null, — или заменить его значением, предоставленным вами (пустой строкой вданном случае)впротивном случае. Оператор ??= присваивает значение переменной, только если она равна null Приработе со значениями null достаточно часто встречается код, который проверяет значение на равенство nullиприсваиваетзначение, отличное отnull, для предотвращения NRE.Например, если вы хотите изменить вашу программу, чтобы она выводила первую строкукода, это можно сделать так: if (nextLine == null) nextLine = "(the first line is null)"; // Этот код работает с nextLine при условии, что переменная отлична от null. Этуусловную команду можно переписать с использованием оператора??=: nextLine ??= "(the first line was empty)"; Оператор ?== проверяет переменную,свойство илиполевлевойчастивыражения(вдан- номслучае nextLine) наравенствоnull.Если леваячастьравна null, то оператор присваивает значение вправойчастивыражения, а еслинет —исходное значение остаетсянеизменным. Мы включили поддержку типов, не до- пускающих null. Теперь компилятор C# говорит, что строка nextLine до- пускает null и может содержать null при обращении к свойству Length. String. Empty — статическое поле класса String, воз- вращающее пустую строку "".
652 глава 11 типы значений, допускающие null Безопасная работа с типами значений, допускающими null Когда вы объявляетеint, bool или другой тип значения и не задаете исходное значение, среда CLR присваивает значение поумолчанию — например,0 или true. Но представьте, что вы пишете код для обработкисетевыхопросовпользователей,вкотором имеетсянеобязательныйвопрос «да/нет». Полу- чается,что вам нужно представить логическое значение, которое можетбыть равно true или false, но может вообще не иметь никакого значения? Издесь вам могут пригодиться типы значений, допускающие null.Тип значения, допускающий null, со- держит либо определенное значение, либо null.В нем используется обобщеннаяструктураNullable<T>, которая может использоваться вкачестве оберткидля значения(или может содержать значение и предо- ставлять компоненты дляобращенияиработы сним).Еслиприсвоить null типузна- чения, допускающемуnull, он небудет иметьопределенногозначения, ноNullable<T> предоставляет удобные компонентыдлябезопаснойработыс нимдажевэтомслучае. Логическое значение, допускающее null, объявляется так: Nullable<bool> optionalYesNoAnswer = null; В C# также предусмотрена сокращенная запись: для типа значения T структуру Nullable<T> можно объявить в виде T?: bool? anotherYesNoAnswer = false; Структура Nullable<T> содержитсвойствоValue, которое задает илиполучает зна- чение. Типbool? содержит значение типа bool, int? содержит значение типа int, и т. д . Также он содержит свойство с именемHasValue, которое возвращает true, еслизначение отлично отnull. Такжетип значениявсегда можно преобразовать к типу, допускающему null: int? myNullableInt = 9321; Чтобы получить значениеобратно,воспользуйтесьудобным свойствомValue: int = myNullableInt.Value; НовызовValueвконечном итоге простопреобразует тип к(int)myNullableInt, иесли значениеравноnull,будет выданоисключениеInvalidOperationException. Именно поэтомуNullable<T> также содержитсвойствоHasValue,котороевозвращает true, если значениеотлично отnull, или false в противном случае. Также можно воспользоваться удобным методом GetValueOrDefault, который безопасновозвращает значение по умолчанию, если Nullable не содержит значения. Также возможно передать значениепо умолчанию иливоспользоватьсянормальным значением поумолчанию длятипа. Nullable<bool> Value: DateTime HasValue: bool ... GetValueOrDefault():DateTime ... T? является синонимом для Nullable<T> Когда к любому типу значения присоединяется вопросительный знак (например, int? или decimal?), ком- пилятор преобразует его в структуру Nullable<T> (Nullable<int> или Nullable<decimal>). В этом нетрудно убедиться: добавьте в программу переменную Nullable<bool>, установите точку прерывания и включите отслеживание этой переменной в отладчике. Вы увидите, что в окне Watch в IDE выводится bool?. Это пример синонима — и не первый, с которым вы встречаетесь в книг е. Наведите указатель мыши на лю- бое вхождение int. Вы увидите, что он преобразуется в структуру с именем System.Int32: intParse() и TryParse() являются компонентами этой структуры. Проделайте это для каждого из типов, упомянутых в начале главы 4. Обратите внимание на то, что все они являются синонимами для структур, кроме строк, которым соответствует класс System. String, а не тип значения. Nullable<T> — структура, по- зволяющая сохра- нить тип значения ИЛИ значение null. На диаграм- ме представлены некоторые ме- тоды и свойства Nullable<bool>.
дальше 4 653 см ерть об ъекта Ключевое слово as может использоваться с объектами. Поддержка полиморфизмаобъектамиоснована на том,чтообъектможетфункционировать как любойиз объектов,откоторых оннаследует. 3 Кэтому моменту выуже должны достаточно понимать, что происходит сослабевшим и не столь могущественным КапитаномВеликолепным. Соб- ственно, это вообще не КапитанВеликолепный, аупакованнаяструктура: Капитан... уже не такой Великолепный Струк- тур а и Структуры не могут наследовать от классов. Неудивительно, что суперсилы Капитана не впечатляют! Он неполучил никакогоунаследо- ва нно го п о ведения. 1 О бъ ект ы не копирую тс я при прис ваива нии. Когда вы присваиваете одной объектной пере- менной другую, вы копируетессылку на ту же пер ем енну ю. 4 Структуры копируются по значению. Этоодна изсамыхполезныхособенностейструк- тур.Она особенно полезна для инкапсуляции. 2 Простота копирования — одно из преимуществ структур (и других типов значений). О б ъ е к т Важный момент: при помощи ключевого слова «is» можно проверить, реализует ли структура заданный интерфейс, — это один из аспектов полиморфизма, поддерживаемых структурами. ТЕМ ВРЕМ ЕН ЕМ В ЛАБОРАТОРИИ КАЖЕТСЯ, Я ЗНАЮ, КАК ПЕРЕДАТЬ ЕГО СИЛЫ ОБЫЧНОМУ ГРАЖДАНИНУ. ЭССЕНЦИЯ В ЕЛИКОЛЕПИ Я
654 глава 11 В: Вернемся чуть назад. Зачем мне вообщезнатьо стеке? О:Затем, что понимание различиймежду стекоми кучейпозволяет правильнопонять, какработаютссылочныетипы и типы значе- ний.Легкозабыть, что структуры и объекты работают по-разному:когда вы используете с ними знак =, они выглядят очень похоже. Понимание того, как работают некоторые механизмы в .NET и CLR во внутренней реализации, поможет разобраться в том, почему ссылочные типы отличаются от типов значений. В:А упаковка? Почему она важна для меня? О: Потому, что вы должны знать, когда данныепопадают встек икогдапроисходит их копирование. Упаковка требует допол- нительных затрат памяти и времени. Если она выполняется в вашей программе всего несколько раз(или несколькосотенраз),вы не заметите разницы. Но представьте, что вашапрограмма вциклевыполняетоднуи ту же операцию миллионыраз в секунду.При- мер не такойужнадуманный: в играхUnity может происходить именно это. Если вы обнаружите, что вашипрограммы занимают все больше памяти или начинаютработать всемедленнее,возможно,их эффективность можно повысить за счет предотвращения упаковки в многократно повторяющихся частях программы. В: Японимаю,как мы получаем новую копию структуры, когда одна структур- ная переменная присваивается другой. Но зачем мнеэто можетпонадобиться? О: Одна из областей, в которых этодей- ствительно полезно, — и н ка псуляция. Взглянитенаследующий код: private Point location; public Point Location { get { return location; } } Если бы поле Point было классом, такая инкапсуляциябылабы простоужасной.Объ- явлениеlocationсprivateниначто невлияет, потому что вы создаете открытоесвойство, доступное только для чтения, которое воз- вращает ссылку наlocation, — и вы х одит, чтолюбойдругой объект сможетобратиться к приватнымданным. Ксчастью, Point является структурой.Аэто означает, что открытое свойство Location возвращает новую копию данных. Объект, которыйиспользуетэтукопию, можетделать с нейвсе чтоугодно — эти измененияникак не отразятся на приватном полеlocation. В: Как узнать, когда лучше использо- вать структуру, а когда класс? О: В большинстве случаев программи- сты используют классы. У структур много ограничений, которые сильно усложняют работу с нимив крупномасштабных проектах. Структуры неподдерживают наследование илиабстракцию,поддержкаполиморфизма ограниченна,а выужезнаете,насколькоэто важно для написания кода. Ст ру кту ры по -нас то ящему по лезны для не- больших, ограниченных типов данных, с кото - рымивыполняютсяповторяющиесяоперации. Хор ошим приме ром с лу жат век торы Uni ty ; в некоторых играх они используются снова и снова, иногда миллионы раз. Повторное использование вектора с присваиванием его той ж е переменн ой при в одит к пов то рному использованию той же памяти в стеке. Если быVector3 былклассом, то CLRпришлосьбы выделять память в кучедля каждого нового экземпляра Vector3, а в системе постоянно происходилабы сборка мусора. Таким образом, реализовав Vector3 в виде структуры, а не класса, группаразработчиковUnityобеспечила более высокую частоту кадров ввашихиграх, а вам для этого не пришлось ничего делать. Структуры приносят пользу при инкапсуляции, потому что свойство, доступное только для чтения, которое возвращает структуру, всегда создает его новую копию. структуры для инкапсуляции Предполагается,чтоэтотметодбудетуничтожатьобъектEvilClone, помечая егодля сборщика мусора,ноонне работает.Почему? void SetCloneToNull(EvilClone clone) => clone = null; часто Зада вае мые вопросы M INI Возьми вруку карандаш
дальше 4 655 см ерть об ъекта У бассейна Ваша задача — взять фрагменты кода из бассейна и разме- стить их в пустых строках. Любой фрагмент можно ис- пользовать несколько раз, использовать все фрагмен- ты не обязательно. Ваша цель —добиться того, чтобы программа вывела на консоль приведенныйнижерезультат. Каждый фрагмент можно использовать несколько раз! class Program { static void Main(string[] args) => new Faucet(); } public class Faucet { public Faucet() { Table wine = new Table(); Hinge book = new Hinge(); wine.Set(book); book.Set(wine); wine.Lamp(10); book.garden.Lamp("back in"); book.bulb *= 2; wine.Lamp("minutes"); wine.Lamp(book); } } public struct Table { public string stairs; public Hinge floor; public void Set(Hinge b) => floor = b; public void Lamp(object oil) { if (oil is int oilInt) floor .bulb = oilInt; else if (oil is string oilString) stairs = oilString; else if (oil is Hinge vine ) Console.WriteLine( $"{vine.Table()} { floor .bulb} {stairs}"); } } public class Hinge { public int bulb; public Table garden; public void Set(Table a) => garden = a; public string Table() { return garden .stairs; } } public private class new ab st ract interface struct string int float single double if or is on as oop + - ++ gar den floor Window Door vin e Hinge Br ush Lamp bulb Tabl e stairs Дополнительный вопрос : обведите те строки, в которых в ыполня е тс я упа ковка. -- = == back in 20 minutes Результат Рез уль та т, который долж- но выводить приложение.
656 глава 11 методы расширения Этот метод только присваивает nullсвоему параметру, но этот параметр всего лишь является ссылкой наEvilClone. В сущности, мы прикрепили к объекту наклейку, а потом сняли ее. Параметр clone находится в стеке, так что присваи- вание ему никак не отражается на куче. class Program { static void Main(string[] args) => new Faucet(); } public class Faucet { public Faucet() { Table wine = new Table(); Hinge book = new Hinge(); wine.Set(book); book.Set(wine); wine.Lamp(10); book.garden.Lamp("back in"); book.bulb *= 2; wine.Lamp("minutes"); wine.Lamp(book); } } public str uct Table { public string stairs; public Hinge floor; public void Set(Hinge b) => floor = b; public void Lamp(object oil) { if (oil is int oilInt) floor .bulb = oilInt; else if (oil is string oilString) stairs = oilString; else if (oil is Hinge vine ) Console.WriteLine( $"{vine.Table()} { floor .bulb} {stairs}"); } } public class Hinge { public int bulb; public Table garden; public void Set(Table a) => garden = a; public string Table() { return garden .stairs; } } Убассейна. Решение Вот почему тип Table объявлен как структура. Если бы он был классом, то поле wine указывало бы на тот же объект, что и book.Garden, в результате чего эта команда перезаписала бы строку "back in". Метод Lamp присваи- вает различные строки и int. Если вызвать его с аргументом int, он присвоит значение поле Bulb в объекте, на ко- торый указывает Hinge. Если передать Lamp строку, то полю Stairs присваивается текущее со- держимое этой строки. Hinge и Table содержат методы Set, тело которых определяется выра- жением. Hinge.Set задает поле Table с именем Garden, тогда как Table.Set задает поле Hinge с именем Floor. back in 20 minutes Результат Предполагается,чтоэтот методбудетуничтожатьобъектEvilClone, помечая его для сборщика мусора,но онне работает.Почему? void SetCloneToNull(EvilClone clone) => clone = null; Дополнительное за да ние: обведите строки, в которых проис ходит у паковка . Так как метод Lamp получает параметр object, при пере- даче int или строки автоматически выполняется упаковка. С другой стороны, book не упаковывается, потому что уже является объектом — экземпляром класса Hinge. Возьми в руку карандаш Решение
дальше 4 657 см ерть об ъекта sealed class OrdinaryHuman { private int age; int weight; public OrdinaryHuman(int weight){ this.weight = weight; } public void GoToWork() { /* Код для выхода на работу */ } public void PayBills() { /* Код для оплаты счетов */ } } static class AmazeballsSerum { public static string BreakWalls(this OrdinaryHuman h, double wallDensity) { return ($"I broke through a wall of {wallDensity} density. "); } } Иногда требуетсярасширить класс, наследование откоторого невозможно,например запечатанного (sealed)класса(многие классы .NET являются запечатанными и не могут использоваться для наследо- вания).C#предоставляетдляподобных случаевгибкийинструмент:методы расширения. Когда выдо- бавляетевсвойпроект классс методамирасширения, он добавляетновыеметоды вуже существующие классы. От вас потребуется лишь создать статический класс и добавить статический метод, который получает экземпляр класса впервом параметрес использованием ключевого слова this. Допустим,у вас имеетсязапечатанный классOrdinaryHuman: Методы расширения добавляют новое поведение в СУЩЕСТВУЮЩИЕ классы Методы расширения всегда являются статическими, и они должны размещать- ся в статических классах. Так как расширяется класс OrdinaryHuman, первый па- раметр объявляется в виде «this OrdinaryHuman». Класс OrdinaryHuman объявлен запечатанным и субклассироваться не может. Но что, если нам потребуется доба- вить в него метод? Метод AmazeballsSerum добавляет метод расширения в OrdinaryHuman: Как только класс AmazeballsSerum будет добавлен в проект, уOrdinaryHuman появляется метод BreakWalls. Теперь вы можете использовать его всвоем методе Main: static void Main(string[] args){ OrdinaryHuman steve = new OrdinaryHuman(185); Console.WriteLine(steve.BreakWalls(89.2)); } Чтобы использовать метод расширения, определите первый параметр с ключевым словом «this». Помните модификатор sealed из главы 7? Он указывает, что класс не может расширяться. Вотивсе!Отвас требуетсялишь добавить классAmazeballsSerum вваш проект, и внезапно в классе OrdinaryHuman появляется новенький метод BreakWalls. Ранее в этой книге мы «по волшебству» добавляли методы в классы простым включением директивы using в начало кода. А вы помните, где это было? Когда программа соз- дает экземпляр класса OrdinaryHuman, она может об- ратиться к методу BreakWalls напрямую — при условии, что класс AmazeballsSerum входит в проект. Убедитесь сами! Создайте новое консольное приложение, добавьте в него два класса и метод Main. Зайдите в отладчике в метод BreakWalls и посмотрите, что в нем происходит.
658 глава 11 использование методов расширения В: Объясните, почему бы недобавить нужныемне методы прямо в код моего класса,вместо того чтобы использовать методы расширения? О:Выможететакпоступить,инаверное,этостоит сделать,если речь идет только одобавлении метода в один класс. Методами расширения следуетпользоватьсяосмотрительно, итольков тех случаях, когда изменение класса, с которым вы работаете, по какой-то причине абсолютно невозможно(например, если он является частью .NET Framework илидругогостороннего кода.) Методы расширения по-настоящему проявляют свою мощь тогда, когдавамтребуетсярасширить поведение того, что вам обычно недоступно, например типа или объекта, входящего в .NET Framework или другуюбиблиотеку. В:Зачем вообщеиспользоватьметоды расширения? По- чему бы не расширить класс наследованием? О:Есливыможетерасширить класс,то обычно стоит поступить именнотак —методы расширения не предназначеныдля замены наследования. Но обычно они оказываются очень удобными, когда у вас имеются классы, которые невозможнорасширить. Сметодамирасширения вы можете изменить поведениецелых групп объектов идажедобавлять новуюфункциональность в не- которыефундаментальные классы .NET Framework. Расширениекласса предоставляет новое поведение,но вам при- детсяиспользоватьновыйсубкласс,есливы хотитеиспользовать это поведение. В:Влияет ли мой метод расширения на все экземпляры класса или только на какой-то отдельный экземпляр? О:Навсеэкземплярырасширяемого класса.Болеетого,после того, как вы создадите метод расширения, он появится в IDE наряду с обычными методами расширяемого класса. Когда я добавил директиву «using system.linq» в начало кода, во всех моих коллекциях ипоследовательностях неожиданно появились методыLINQ. Выходит, я использовал методы расширения? И еще один факт, который стоит помнить: создание метода расширения не предоставит вам доступа к внутрен- нему устройству кл асс а. Эта кнопка в окне IntelliSense выводит только методы расширения. Enumerable.First — метод расширения, в объявлении которого используется ключевое слово «this». Да! В основе LINQ лежат методы расширения. Кромерасширенияклассов,такжеможнорасширять интерфейсы.Все,что дляэтого нужно, — использовать имя интерфейса вместо класса после ключевого слова this в первом параметре метода расширения. Методрасширениябудет добавляться вкаж- дый класс, реализующий этотинтерфейс. Именно так поступила команда .NET при создании LINQ: всеметоды LINQ являются статическими методами расширения для интерфейса IEnumerable<T>. Когда вы включаете директиву using System.Linq; в начало своего кода, ваш код «ви- дит» статическийкласс сименемSystem.Linq.Enumerable. Мыиспользовалинекоторые из его методов (например,Enumerable.Range), но он также содержит методы расшире- ния. Откройте IDE, введите имя Enumerable.First и просмотрите объявление. Оно начинаетсяс описания (extension), которое сообщает, что это метод расширения,а его первыйпараметр использует ключевое словоthis, какивнаписанном вамиметоде расширения. Эта схема применяется во всехметодахLINQ. часто Зад авае м ые вопросы
дальше 4 659 см ерть об ъекта Чтобы изучитьработуметодоврасширения, расширим классString.Создайте новый проектконсольного приложения идобавьтефайл с именемHumanExtensions.cs . Расширени е фунд аме нтального ти па: stri ng Разместите все методы расширения в отдельном пространстве имен. Всеметодырасширенияжелательноразместить вспециальномпространствеименотдель- но от основного кода. Притаком подходевы сможете легко найти их для использования в другихпрограммах.Создайтестатическийкласс,в которомбудут находитьсяваши методы: 1 namespace AmazingExtensions { public static class ExtendAHuman { Создайте статический метод расширения, определите его первый параметр c ключевым словом this и расширяемым типом. Дваглавныхфакта, окоторыхнеобходимо помнить приобъявлении методарасширения:ме- тод должен быть статическим ион должен получатьрасширяемыйклассв первомпараметре: 2 Завершите метод расширения. Метод проверяет, содержит ли строка слово «Help!» (просьба о по- мощи,на которую должен откликнуться каждыйсупергерой): 3 Используйте новый метод расширения IsDistressCall. Добавьте директиву using AmazingExtensions; в началофайла с классомProgram. Затем добавьте в класс код, который создаетстроку и вызывает метод IsDistressCall.Выувидите метод расширения в окне IntelliSense: 4 if (s.Contains("Help!")) return true; el se return false; } } } public static bool IsDistressCall(this string s) { Использует метод String.Contains для про- верки того, содержит ли строка слово «Help!» — определенно это не та операция, которая обычно выполняется со строками. Использование отдельного простран- ства имен — хороший организационный прием. Кроме того, класс, в котором определяется ваш метод расширения, должен быть статическим. Метод расшире- ния тоже должен быть статиче- ским. «this string s» означает, что мы расширяем класс String, а пара- метр s использу- ется для обраще- ния к используемой строке для вызова метода. Проследите за тем, чтобы класс был объ- явлен откры- тым (а сле- довательно, видимым при добавлении объявления using). Как только вы доба- вите директиву using для включения про- странства имен со статическим классом, в окне IntelliSense по- явится новый метод расширения. Именно по этому принципу работает LINQ. Do this! Сдела й те это!
660 глава 11 смерть — еще не конец Развлечения с магнитами Расставьте магнитытак,чтобыпрограммавыводила следующий результат: a buck begets more bucks } } namespace Upside { public static class Margin { public static void SendIt (this string s) { } Console.Write(s); public static string ToPrice (this int n) { if(n==1) return "a buck "; else return " more bucks"; public static string Green (this bool b){ if (b == true) return "be"; e lse return "gets"; } } } static void Main(string[] args) { int i = 1; string s = i.ToPrice(); s.SendIt(); bool b = true; b.Green().SendIt(); b = false; i=3; i.ToPrice() .Se nd It( ); } } b.Green().SendIt(); class Program { namespace Sideways { using Upside;
дальше 4 661 см ерть об ъекта TheUNIVERSE НОВАЯ ЖИЗНЬ КАПИТАНА ВЕЛИКОЛЕПНОГО СМЕРТЬ ЕЩЕ НЕ КОНЕЦ Лаки Бернс ШТАТНЫЙ КОРРЕСПОНДЕНТ ОБЪЕКТВИЛЬ КапитанВеликолепный десериализует себя—потрясающеевозвращение кжизни! Послеумопомрачительного поворотасобытий Капитан Великолепный вернулся в Объектвиль. В прошлом месяце оказалось, что гроб Капитана пуст, а там, где должно было лежатьего тело, лежала только странная записка. Анализ записки показал, что ДНК Капитана Великолепного — все ее поля и значения — была точно отражена в двоичном формате. Сегодня эти данныенеожиданно воплотились в жизнь.Капитан вернулся —ему удалось десериализовать себя по своей записке. Когда мыего спросили, как ему пришел в голову такой план, Капитан только пожал плечами и пробормотал: «Глава 10». Источники, близкие к Капитану, отказались комментировать смысл этой загадочной реплики, но признали, что до своего неудачного нападения на ПройдохуКапитанпровел многовремениза чтениемкниг,изучаяметодыDisposeи тему долгосрочного храненияданных.Мыожидаем, что Капитан Великолепный... К апит ан Вел иколе пный вернулся! МЫ ВОССТАНОВИЛИ КЛАСС СУПЕРГЕРОЯ, НО КАК ВЕРНУТЬ КАПИТАНА? ЭВ РИК А! Я ПРОАНАЛИЗИРОВАЛА КОД — КАПИТАН ВЕЛИКО­ ЛЕПНЫЙ ИСПОЛЬЗОВАЛ СВОЮ СМЕРТЬ ДЛЯ ТОГО, ЧТОБЫ СЕРИАЛИЗОВАТЬ СЕБЯ !
662 глава 11 } решен ие упр аж нени я Развлечения с магнитами. Решение Расставьтемагниты так, чтобыпрограмма выводила следующийрезультат: a buck begets more bucks namespaceUpside { public static class Margin { public static voidSendIt (this string s) { } Console.Write(s); public static string ToPrice (thisint n){ if(n== 1) return"a buck "; els e return" morebucks"; } public static string Green (this bool b){ if (b == true) return"be"; els e return"gets"; } } } static voidMain(string[] args) { inti=1; string s = i .ToPrice(); s. Sen dIt( ); bool b =true; b.Green().SendIt(); b =false; i=3; i.ToPrice() .S endIt (); } } Пространство имен Upside имеет расширение. Про- странство имен Sideways содержит точку входа. Метод Main расширяет bool — он возвращает строку "be", если bool содержит true, или "gets" для значения false. Метод Main использует расширения, добавленные в класс Margin. Класс Margin расширяет строку и добав- ляет метод Sendit, который просто вы- водит строку на консоль. Также он рас- ширяет int и добавляет метод ToPrice, который возвращает строку "a buck", если значение int равно 1, или "more bucks" в противном случае. b.Green().SendIt(); class Program { namespaceSideways { u sing Ups ide;
Я знаю, что этот страшный клоун где-то прячется. Хорошо, что я написала код для обработки СтрахИсключение. Обработка исключений 12 Борьба с огнем надоедает Программисты не должны уподобляться пожарным. Вы усердно ра- ботали, штудировали справочники и руководства и, наконец, достигли вершины. Но вам до сих пор продолжают звонить с работы по ночам, потому что програм- ма упала или работает не так, как должна работать. Ничто так не выбивает из колеи, как необходимость устранять странные ошибки... но благодаря обра- ботке исключений вы сможете написать код, который сам будет разбираться с возможными проблемами. А еще лучше, что вы можете планировать такие проблемы и восстанавливать работоспособность программы при их воз- ни кно вен ии.
664 глава 12 Программа вывода шестнадцатеричного дампа читает имя файла из командной строки В конце главы 10 мы построили программу вывода шестнадцатеричного дампа, которая использовала аргументы командной строки для вывода произвольногофайла. Мы восполь- зовались свойствами проекта вIDE для задания аргументоввотладчике. Также выувидели, как запустить программу из команднойстрокиWindows илиокна терминала macOS. Необработанное исключение озна- чает, что приложение столкнулось с непредвиденной проблемой. программа выводадампа выдает исключение Но что произойдет, если передать HexDump недействительное имя файла? Когда вы изменяли приложениеHexDump для поддержкиаргументов командной строки,мы рекомен- довали особенно внимательно следить за тем, чтобы программе передавалось правильное имяфайла. Чтопроизойдет, еслифайл суказанным именем несуществует?Попробуйте запустить приложение, но теперь передать емуаргументinvalid-filename. На этотраз программа выдает исключение. Задайтев настройкахпроекта недействительноеимяфайлаи запуститеприложение в отладчикеIDE. Вэтомслучаебудет выдано исключение с темже именем класса (System.IO.FileNotFoundException) ипохожее сообщение«Файл ненайден». ...и тр ас- сировку стека. С каждым исключением связано имя класса и сообщение... IDEостанавливает отладчик в строке, в ко- торой произошло исключение, и выводит информацию в окнеException Unhandled. Вы даже сможете просмотреть трассировку стека в окне Call Stack.
дальше 4 665 обработка исключений cl ass H oneyB ee { public double Capacity { get; set; } public string Name { get; set; } pub lic H oneyB ee(do uble capac ity, strin g nam e) { Capa city = cap acity ; Name = na me; } pub lic s tatic void Main (stri ng[] args) { obje ct my Bee = new Honey Bee(3 6.5, "Zipp o"); floa t how MuchH oney = (fl oat)m yBee; Hone yBee anoth erBee = ne w Hon eyBee (12.5 , "Bu zzy") ; doub le be eName = do uble. Parse (anot herBe e.Nam e); double totalHoney = 36.5 + 12.5; string beesWeCanFeed = ""; for (int i = 1; i < (int)totalHoney; i++) { beesW eCanF eed + = i.T oStri ng(); } int numbe rOfBe es = int.P arse( beesW eCanF eed); int drones = 4; int queens = 0; int drone sPerQ ueen = dro nes / quee ns; anot herBe e = n ull; if ( drone sPerQ ueen < 10) { anoth erBee .Capa city = 12. 6; } } } Этот код не работает. Программа выдает пять разных исключений; справа приведены сообщения об ошибках, выводимые в IDE или на консоль. Ваша задача — соединить строки, содержащие ошибки, с исключением, которое генерируется этими строками. Помните, Метод doubleParse преобра- зует строку в double, по- этому при передаче строки (например, "32.7") будет возвращено эквивалентное значение double (32.7). Как вы думаете, что произой- дет при передаче строки, которая не может быть преобразована в double? 3 2 1 4 5 что код не работает. Если добавить этот класс в приложение и запустить его метод M ain, в ыпол нение пре рв ет ся после первого выданного исключения. Просто прочитайте код и сопоставьте каждое исключение со строкой, в кото - рой оно было бы выдано при запуске. Возьми в руку карандаш
666 глава 12 HoneyBee anotherBee = new HoneyBee(12.5, "Buzzy"); double beeName = double.Parse(anotherBee.Name); Вам былопредложено соединить каждую проблемную стро- кукода сисключением, которое генерируетсяэтойстрокой. Код преобразования myBee к типу float компи- лируется, но способа преобразования объекта HoneyBee во float не существует. При выпол- нении кода CLR понятия не имеет, как выпол- нить это преобразование, поэтому выдается исключение InvalidCastException. Метод Parse ожидает, что вы предоставите ему строку в опре- деленном формате. Когда вы передаете ему строку "Buzzy", он не знает, как преобразовать ее в число, и поэтому выдает исключение FormatException. возьми в руку карандашрешение double totalHoney = 36 .5 + 12.5; string beesWeCanFeed = ""; for (int i = 1; i < (int)totalHoney; i++) { beesWeCanFeed += i .ToString(); } int numberOfBees = int.Parse(beesWeCanFeed); Цикл for создает стро- ку с именем beesWeCanFeed с числом, состоящим более чем из 60 цифр. Такое боль- шое число никак не может поместиться в int, и по - пытка преобразования его к int приведет к выдаче OverflowException. Все эти исключенияникогда не будутвыданы подряд — программа выдает первое иск л ючен ие и остан авл и вается. Второе исключение встретится вам только в том случае, если вы исправите причину первого. 3 object myBee = new HoneyBee(36.5 , "Zippo"); float howMuchHoney = (float)myBee; Чтобы воспроизвести эти исключения в IDE, скопируйтеэтот код и запустите его. Установите точку прерывания в первой строке кода, затем щелкните правой кнопкой мыши в строке кода, которую вы хотите выполнить, и выберите команду Set NextStatement — при продолжении в ы полнен ия прил ожение пер ейдет с разу к ук азанн ой кома нде. Подсказки для IDE: Команда Set Next Statement 5 1 Возьми в руку карандаш Решение
дальше 4 667 обработка исключений Присваивание null переменной anotherBee сообщает C#, что она ни на что не указывает. Выдавая исключение NullReferenceException, C# сообщает вам, что не существует объекта, для ко- торого можно было бы вызвать метод D oMy Job. anotherBee = null; if (dronesPerQueen < 10) { anotherBee.Capacity = 12 .6; } int drones = 4; int queens = 0; int dronesPerQueen = drones / queens; Ошибки DivideByZero можно было бы избежать. Дажееслипросто взглянуть на код, становится понятно, что здесь что-то не так. Если задуматься, то же самое от- носится и к другимисключениям — вся сутьупражнения «Возьми вруку карандаш» заключалась в поиске этих ис- ключений без запуска кода. Каждоеиз этих исключений можно было предотвратить. Чем больше вы знаете об исключениях,тем лучшевыбудетесправляться с предот- вращением сбоев в приложениях. Сгенерировать исключение DivideByZeroException несложно — для этого достаточно разделить любое число на нуль. При делении любого целого числа на нуль всегда выдается такое исключение. Даже если значение queens неизвестно заранее, исклю- чение можно предотвратить — проверьте, не равно ли значение нулю, прежде чем делить на него. 4 Даже при простом взгляде на код я вижу, что он пытается делить на нуль. Уверен, что этого исключения можно избежать. 2 Возьми в руку карандаш Решение
668 глава 12 об ъекты иск л ючени й О б ъ е к тE x c e p t i o n Когда ваша программа выдает исключение, CLR генерирует объект Exception Мырассматривали механизм,который используетсяCLR для опо- вещения отом,что впрограммечто-топошло нетак: исключения . Когда в программе происходит исключение, создается объект, представляющий проблему. Как нетрудно предположить, это объ- ект класса Exception. Допустим,у вас имеется массивс четырьмя элементамии вы пыта- етесь обратиться к 16-му элементу (с индексом 15, так как индексы начинаются с нуля): ис-клю-че-ни-е, сущ. Человек или объект, ис - ключаемые из общего утверждения или не под- чиняющиеся общему пра- вилу. int[] anArray = {3, 4, 1, 11}; int aValue = anArray[15]; CLRидет на все хлопоты с созданием объекта,потому что среда должна предо- ставить полную информацию о причине возникновенияисключения. Возможно, вам придется внести изменения в коде или какие-то исправления в обработку конкретнойситуации ввашей программе. В данном случае выдается исключение IndexOutOfRangeException, которое раскрывает суть проблемы:вы пытаетесь обратитьсякэлементус индексом,вы- ходящим за границу массива. У вас же имеется информация о том,гдеименно вкоде возникла проблема;этоупростит ее поискиисправление(даже есливаша программа содержиттысячистрок кода). Объект Exception со- держит сообщение с описанием проблемы и трассировкой стека, т. е. списком всех вызо- вов, приведших к коман- де, в которой произошло исключение. Как только в вашей программе произойдет исключение, CLR создает объект с полной инфор- мацией о проблеме. Очевидно, в этом коде возникнет проблема. Когда IDE останавливает выполнение из-за того, что в коде произошло исключение, вы можете просмотреть подробную информацию об исключении вразделе $exception в окне Locals. В этом окневыводятсявсе переменные,находящиеся вобласти видимости(т. е. доступныедля текущейкоманды).
дальше 4 669 обработка исключений Все объекты Exception наследуют от System.Exception .N ET поддерживает множестворазныхисключений, о которыхприходитсясообщать пользователю.Так какмногиеиз нихобладаютрядомсходныхпризнаков, вигрувступаетмеханизм наследования. В .NET определяется базовыйклассException,откоторого наследуютвсеконкретныетипы исключений. В классе Exception есть пара полезных компонентов. Свойство Message содержитудобочитаемое со- общение о проблеме. Свойство StackTrace сообщает, какой код выполнялся в момент возникновения исключенияикакая последовательность вызововпривела кисключению.(Существуютидругиесвойства, но пока мы ограничимся этими.) In dexO utO fR ange E xcept ion Message StackTrace GetBaseException ToString For m atE xcept ion Message StackTrace GetBaseException ToString O ver flo wE xcept ion Message StackTrace GetBaseException ToString Exception Message StackTrace GetBaseException ToString Класс Exception может расширяться, как и любой другой класс. Вы даже може- те создавать собственные классы исключений, а так- же переопределять Message и другие свойства и методы Exception. Очень удобно, что .NET предоставляет столько типов исключений, по - тому что разные исклю- чения выдаются в разных ситуациях. Вы можете много узнать о непред- виденной ситуации, по- родившей исключение, по имени самого исключения. ToString генерирует сводку всей информа- ции в полях исключения и возвращает ее в виде стр оки . Div ideByZ ero E xcep tion Message StackTrace GetBaseException ToString Ваше приложениеHexDump выдает исключение, если передать в командной строке недействитель- ное имя файла. Как можно справиться с этой про- блемой? Мозговой штурм
670 глава 12 исключения помогают обработать непредвиденные ситуации Отладка любого исключения должна начаться с изучения всей полученной информации. При выполнении консольного приложения эта информация выводится на консоль. Повнимательнее присмотримся к диагностической информации, вы- веденной приложением(мы переместили приложение в папку C:\HexDump, чтобы сократить пути в трассировке стека): C:\HexDump\bin\Debug\netcoreapp3.1> HexDumpinvalid-filename Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\HexDump\bin\Debug\ netcoreapp3.1\invalid-filename'. File name: 'C:\HexDump\bin\Debug\netcoreapp3.1\invalid-filename' at System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle) at System.IO.FileStream.CreateFileOpenHandle(FileMode mode, FileShare share, FileOptions options) at System.IO.FileStream..ctor(String path, FileMode mode, ... , FileOptions options) at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share) at System.IO.File.OpenRead(String path) at HexDump.Program.Main(String[] args) in C:\HexDump\Program.cs:line 12 Мы заметили в этой трассировке следующее: • Класс исключения:System.IO .FileNotFoundException • Сообщение об исключении: Could notfind file 'C:\HexDump\bin\Debug\netcoreapp3.1\invalid-filename' . • Дополнительная диагностическая информация:File name: 'C:\HexDump\bin\Debug\netcoreapp3.1\invalid-filename' • Первые пять строк в трассировке стека относятся к классам из пространства имен System.IO • Последняя строка трассировки стека относится к нашему пространству имен HexDump, и она включает номер строки в программе. Эта строка выглядит так: using (Stream input = File.Opened(args[0])) Воспроизведите ошибку в отладчике В конце главы10 мы показали, как задать аргументы приложения при запуске приложения в отладчике. Задайте значение invalid-filename, затем установите точку прерывания в строке приложения, в которой произошло исключение. За- пустите приложение, и когда управление будет передано в точку прерывания, выполните командув пошаговом режиме. В IDE выводится информация об исключении. Добавьте код для предотвращения исключения Приложение выдает исключение, потому что оно пытается прочитать данные из несуществующего файла. Чтобы предот- вратить исключение, следуетсначала проверить, существует ли файл. Если он не существует, то вместо открытия потока с содержимым файла вызовом File.OpenReadбудет использован вызовConsole.OpenStandardInput, возвращающий поток стандартного ввода вашего приложения. Начните сдобавления метода GetInputStream в приложение: static Stream GetInputStream(string[] args) { if ((args.Length != 1) || !File.Exists(args[0])) return Console.OpenStandardInput(); els e return File.OpenRead(args[0]); } Затем измените строку, в которой выдавалось исключение, и включите в нее вы- зов нового метода: using (Stream input = GetInputStream(args)) Теперь запустите приложение вIDE. На этот раз оно не выдает исключение, а читает данные из стандартного ввода. Про- тестируйте его: • Введите данные и нажмите Enter. Приложение выводит шестнадцатеричный дамп введенныхданных, завершающих- ся разрывом строки (0d 0aв Windows, 0a на Mac). Поток stdin добавляетданные только после разрыва строки, так что приложение будет выводить новый дампдля каждой строки. • Запустите приложение из командной строки: HexDump << input.txt (или ./HexDump << input.txt на Mac). Приложение направляет данные из файлаinput.txt в поток stdin и выводит дамп всего содержимого файла. По следу Console.OpenStandardInput возвращает объект Stream, связанный со стандартным вводом приложения. Если вы передаете входныеданные приложения по каналу или запустили его в IDE и ввели console или terminal, все введенные или переданные по каналу данныепопадут в поток. Запуская программу из командной строки Mac, не забудьте заново опубликовать ее командой «dotnet publish -r o sx -x64» из каталога решения.
дальше 4 671 обработка исключений В:Так что жетакоеисключение? О:Это объект, который создается CLR при возникновении про- блемы. Также вы можете генерировать исключения всвоемкоде — собственно, вы ужеделали эторанее с ключевым словом throw. В:Исключениеявляется намеренно? О: Да, исключение является объектом. CLR создает его для передачи всей информации о том, что происходило при выполне- нии команды, породившей исключение. Его свойства, которые вы виделипри просмотрерасширенногоописания$exception в окне Locals,содержат информацию обисключении. Например, свойство Messageсодержитполезную строку вида«Попыткаделения нануль» или «Слишкомбольшое или слишком малое значение дляInt.32». В:Почему существует столькоразных объектов Exception? О: Потому, что вариантов непредвиденного поведения вашего кода очень много. Во многих ситуациях ваша программа просто аварийно завершится.Еслибывы незнали, почему произошелсбой, этобы очень сильнозатруднилодиагностику.Выдаваяразныевиды исключений вразных обстоятельствах, CLRпредоставляет много ценной информации, упрощающей поиск и исправление проблем. В: Означает ли это, что когда мой код выдает исключение, это не всегда происходит по моей вине? О: Точно. Иногда входные данные не соответствуют вашим ожиданиям — как в примереработы с массивом, который оказался короче,чемпредполагалось. Крометого, не забывайте, что с ваши- ми программами работают живые люди, поведение которых часто непредсказуемо. При помощи исключений C# упрощает обработку этих непредвиденных ситуаций, чтобы выполнение вашего кода шло гладко и он не выдавал загадочные ибесполезные сообще- ния об ошибке. В:Выходит, исключения должны мне помочь, а не создавать проблемы, из-за которых мне приходится по ночам отлаживать свои приложения? О:Да!Исключения помогают вам ожидать неожиданное. Многие разработчики огорчаются, когда в их коде происходит исключение. Вместоэтогоимстоилобырассматривать исключение как помощь в выявлении проблем и отладке программ со стороныC#. Все верно. Исключения — полезный инструмент для выявления тех мест, в которых поведение вашего кода не соответствует ожидаемому. Многиенеопытныепрограммисты пугаются при виде ис- ключений. Однако исключения очень удобны, и вы можете обратить ихсебе на пользу. Исключение предоставляет вам многочисленные подсказки, по которым вы сможете опре- делить, почему ваш код реагируетнепредвиденным образом. Таквыузнаете о новых ситуациях, которые должны обра- батыватьсявпрограммах, и получаете возможность что-то снимисделать. Похоже,исключения не всегда плохи. Иногдаонипомогают выявлять ошибки,но во многих случаях онилишь сообщают мне,что в программе произошло что-то неожиданное для меня. Вся суть исключе- ний – поиск и исправле- ние ситуаций, в которых поведение ва- шего кода не соответствует ожиданиям. часто Задава ем ые вопросы
672 глава 12 вызов небезопасного кода Для некоторых файлов вывод дампа невозможен Вглаве 9 мы рассказывали, как повысить надежность вашего кода, чтобы он справлялсяс поврежденнымиданными, некорректносфор- мированным вводом, ошибками пользователя идругиминепредви- денными ситуациями.Попыткавыводадампастандартноговвода при отсутствиифайла в командной строке или суказанием несуществую- щегофайла— отличный примерповышениянадежности программы. Остались лиеще какие-то ситуации, которые необходимо обрабо- тать? Например, еслифайл существует, но недоступен для чтения? Посмотрим, чтопроизойдет, еслизапретить чтениефайла, апотом попытаться прочитать его: ≥ В Windows: щелкните правой кнопкой мыши вПроводнике Windows, выберите команду Свойства, перейдите на вкладку Безопасностьи щелкните на кнопкеИзменить,чтобыизменить разрешениядоступакфайлу.Установите всефлажкиЗапретить. ≥ НаMac: вокне терминала перейдите впапку сфайлом, дамп которого выхотите вывести, ивыполнитеследующуюкоманду, заменив binarydata.datименемфайла:chmod 000binarydata.dat. Теперь,когда сфайла снятыразрешения чтения,попробуйтеснова запустить приложениевIDE или вкомандной строке. Программа выдает исключение: трассировка стека показывает, что команда using вызвала метод GetInputStream, в результате чегоFileStream выдает исключение System.UnauthorizedAccessException: C:\HexDump\bin\Debug\netcoreapp3.1>hexdump binarydata.dat Unhandled exception. System.UnauthorizedAccessException:Access to the path 'C:\HexDump\bin\Debug\ netcoreapp3.1\binarydata.dat'isdenied. at System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle) at System.IO.FileStream.CreateFileOpenHandle(FileMode mode, ..., FileOptions options) at System.IO.FileStream..ctor(String path, ..., Int32 bufferSize, FileOptions options) at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share) at System.IO.File.OpenRead(String path) at HexDump.Program.GetInputStream(String[] args) in C:\HexDump\Program.cs:line 14 at HexDump.Program.Main(String[] args) in C:\HexDump\Program.cs:line20 Одну минуту. Конечно,программа аварийно завершится. Я передаю ей нечитаемыйфайл. Пользователипостоянно ошибаются. И с этим ничего не поделаешь...Не так ли? Вообще говоря, кое-что сделать можно Да, пользователи постоянно ошибаются. Они вводят неправильную информацию, некорректные входные данные ииз любопытстващелкают на том, о существовании чего вы даженеподозревали. Такова суроваяреальность жизни, ноэто неозначает, что с ней ничего не поделать. C# предоставляет удобные средства обработки ис- ключений, которые повышаютнадежность вашей программы. Хотя вы не можете управлять тем,что пользователиделают свашим приложением,выможетепринять меры к тому, чтобы приложение не завершалось аварийно при их возникновении.
дальше 4 673 обработка исключений 1 Допустим, метод, вызываемый в вашей программе, получает данные от пользователя. 2 Этот метод может делать что-то сомнительное, что не сработает во время вы полнени я. 3 Вы должны знать, что вызываемый метод является небезопасным. Что происходит при вызове небезопасного метода? 4 Вы пишете код, который обрабатывает исклю- чение в случае его воз- никнове ния . Вы прос т о подстраховываетесь на всякий случай. пользователь нап исан ны й вами класс п р е д о с тавляет вход н ы е д а н н ы е в а ш ему м е т о д у public void Process(Input i) { if (i.IsBad()) { Exp lo de( ); } } ваш класс с обработкой исключений publicclassData{ pub lic vo id Pro ces s(I nput i) { if(i.IsBad()){ e xpl ode( ); } } } нап исан ны й вами класс publi c cl ass Data { publicvoid Process(Inputi){ if(i.IsBad()){ expl ode (); } } } publ ic c lass Data { publicvoid Process(Inputi){ if(i.IsBad()){ exp lod e(); } } } н а д е ж ность вашей программ ы п о в ы с и л а с ь ! вхо дн ые данные ̇∆å̊ß∂ıÏÔ̊œ∑ˆ øƒ¥∂∫√̊Ω∆¬̇√̊ ÔÒÎ̇̊∆¬åߥ∂ÒÅ ∆̊åƒ̇ß∂∆̇å∆̊ß ƒå∂ß̇̊ƒ∆̊å∂ß∂ ́̇®£√•√∂̈∂¬∆ƒ ƒ̃å∂√̊祃́∂ˆ́ ∂å̊∆ƒ́∫®̊́̈√∂ publicclassData{ pub lic vo id Pro ces s(I nput i) { try{ if(i.IsBad()){ e xpl ode( ); }catch{ Ha ndl eIt( ); } } } Пользователинепредсказуемы.Они передаютпрограмме всевозможные аномальные данные итворят такое,чего вы не ожидали. И это нормально,потому что с исключениями в коде можносправитьсяприпомощимеханизма обработкиисключений.Он позволяет написать специальный код,которыйбудет выполняться привыдаче исключения. Некоторые разработчики называют исключения «ошибками времени вы- полнения». Ес ли мой методProcessполучит некорректные данные, он перестанетработать! Интересно, чтобудет, если щелкнуть здесь... Ого, да эта программа действительностабильноработает! Если вы знаете, как выполнить ту же операцию с меньшим риском, чтобы избежать исключения, — это лучший из возможных вариан- тов! Но некоторые риски просто неизбежны, и тогда приходится поступать так: пользователь пользователь н апис анны й вами класс
674 глава 12 добавление обработчиков исключений static Stream GetInputStream(string[] args) { if ((args.Length != 1) || !File.Exists(args[0])) return Console.OpenStandardInput(); e lse { try { return File.OpenRead(args[0]); } catch (UnauthorizedAccessException ex) { Console.Error.WriteLine("Unable to read {0}, dumping from stdin: {1}", args[0], ex.Message); return Console.OpenStandardInput(); } } } Обработка исключений с try и catch Чтобы добавить обработку исключений в своем коде, воспользуйтесь ключевыми словамиtry и catch исоздайтеблок кода,которыйбудетвыполняться при выдачеисключения. Код try/catch фактически сообщает компилятору C#: «Проверьте (try) этот код и при появлении ис- ключенияперехватите(catch)егоэтимкодом». Проверяемаячасть кода называетсяблоком try, а часть, обрабатывающая исключения, — блоком catch.В блоке catch можно выполнять различные действия, например вывести сообщение об ошибке, вместо того чтобыдать программе аварийно завершиться. Ещеразвзглянитена последниетристрокитрассировкистекавсценарииHexDump,чтобыпонять,где следуетразместить код обработкиисключений: at System.IO .File.OpenRead(String path) at HexDump.Program.GetInputStream(String[] args) in Program.cs:line 14 at HexDump.Program.Main(String[] args) in Program.cs:line 20 Исключение UnauthorizedAccessException вызвано строкой GetInputStream, из которой вызывается File.OpenRead. Так как это исключение невозможно предотвратить, изменим GetInputStream для ис- пользования блока try/catch: Мы сознательно несталиусложнять свой обра- ботчикисключений. Сначала вызовConsole.Error выводитстрокув поток ошибок(stderr), сообщая пользователюо произошедшей ошибке, после чего программа возвращаетсякчтению данныхизстан- дартноговвода. Обратитевнимание:вблокеcatch присутствуеткоманда return.Методвозвращает Stream, поэтому и при обработке исключения он все равно должен вернуть Stream; в противном случае компилятор выдаст ошибку «Не все ветви кода возвращаютзначение». Ключевое слово catch сигнализи- рует о том, что блок, следующий непосредственно за ним, содержит обработчик исключения. Разместите код, который может выдать исключение, в блоке try. Если исключения не произойдет, все работает как обычно и команды в блоке catch игнорируются. Если команда в блоке try выдает исключение, то остальная часть блока try не выполняется. Это блок try. Обработка исключений начинается с ключевого слова «try». В данном случае за ним раз- мещается существую- щий код. Если в блоке try выдается исключе- ние, программа немедленно пере- ходит к команде catch и начинает выполнять блок catch. Еслипри выдаче исключения происходит автоматический переход к блоку catch, что про- изойдет с объектами иданными, с которыми вы работалидо выдачи исключения? Мозговой штурм
дальше 4 675 обработка исключений Принцип ы отладки Замените метод GetInputStream в приложении HexDump методом, которыйбыл приведенвыше. 1 Изменитеконфигурацию проекта и задайтеаргумент так, чтобы он содержал путь кфайлу,чтениекоторого запрещено. 2 Установите точку прерывания в первой команде GetInputStream. Запустите отладку проекта. 3 При достиженииточкипрерываниявыполнитенесколькоследующих команд в пошаговом режиме,пока недостигнетевызова File.OpenRead. Выполните следующую команду без захода в метод — приложение переходитк первой строкеблока catch. 4 Продолжайте выполнятьблок catch.Он выводит строку на консоль, после чего возвращает Console.OpenStandardInput и возобновляет выполнение метода Main. 5 Отслеживание передачи управления в try/catch Важный аспект обработки исключений заключаетсяв том,чтокогдакоманда ввашемблоке tryвыдаетисключение, весь остальной код вблоке обходится. Управление немедленно передается первойстрокеблока catch.Отладчик IDEпоможетразобратьсявтом, какработаетэта схема. Пошаговое вы- полнение метода. При достижении File.OpenRead будет выда- но исключение, в результате чего управление передается блоку catch. Точка прерыва- ния, установленная в первой строке GetInputStream.
676 глава 12 Код блока finally выполняется ВСЕГДА При возникновении исключения возможны два варианта развития событий. Необработанное исклю- чениеведетк аварийному завершению программы. В противном случаеуправление переходитк блоку catch. Но что происходит с остальным кодом блока try? Представьте, что в этой части закрывался поток илиосвобождались важныересурсы. Тогда код долженбыть выполнен,несмотря на исключение. На помощь в этой ситуации приходит блокfinally.Онвыполняется всегда,независимо отвыдачи или отсутствияисключения. Воспользуемсяотладчиком иразберемсявтом,какработаетблокfinally. Соз да йт е нов ый прое кт ко нсольн ого приложе ния . Добавьте директиву using System.IO; вначало файла, затем добавьте метод Main: static void Main(string[] args) { var firstLine = "No first line was read"; try { var lines = File.ReadAllLines(args[0]); firstLine = (lines.Length > 0) ? lines[0] : "The file was empty"; } catch (Exception ex) { Console.Error.WriteLine("Could not read lines from the file: {0}", ex); } finally { Console.WriteLine(firstLine); } } Добавьте точку прерывания в первую строку метода Main. Запустите отладкуприложенияивыполняйтеегов пошаговомрежиме. Перваястрокаблока try пытается обратитьсяк args[0], но так как аргументы командной строки не заданы, массив args пуст и программа выдает исключение, а именно System.IndexOutOfRangeExceptionс со- общением «Индекс выходит за границу массива». После вывода сообщения выполняется блок finally, ипрограмма завершается. Задайте аргумент командной строки, содержащий путь к существующему файлу. Используйтесвойства проекта для передачиаргумента командной строкиприложению. При- свойтеему полный путь к существующемуфайлу.Убедитесь втом,что имяфайла несодержит пробелов; впротивном случаеприложениеинтерпретируетегокакдва аргумента.Снова вклю- читеотладкусвоего приложения— послезавершенияблока tryбудет выполненблокfinally. Задайте аргумент командной строки, содержащий путь к несуществующему файлу. Вернитесь к свойствам проекта и изменитеаргумент командной строки, чтобы приложению передавалось имя несуществующего файла. Снова запустите свое приложение. На этотразбудет выдано другое исключение:System.IO.FileNotFoundException. Затем выполняетсяблокfinally. 1 2 3 4 блок finally выполняется всегда Принципы отладки Этот блок finally будет выполнен независимо от того, обнаружит блок try исключение или нет. WriteLine вызывает метод ToString объекта Exception, ко- торый возвращает имя исклю- чения, сообщение и трассировку стека. Исключение появляется в окне Locals, как и прежде.
дальше 4 677 обработка исключений Перехват всех исключений Вытолько чтозаставилисвое приложение выдавать исключения двух типов(IndexOutOfRangeException иFileNotFoundException),и оба типабылиобработаны. Взглянитенаблокcatch: catch (Exception ex) Этоусловиеперехватываетвсеисключения: тип послеблока catchуказывает,какой тип исключений должен обрабатываться, а поскольку всеисключениярасширяют классSystem.Exception, определение типа Exception в условииприказываетблоку try/catchперехватывать любые исключения. Предотвращение перехвата всех исключений при помощи множественных блоков catch Всегда лучшепостараться спрогнозировать конкретные исключения,которыебудутвыдаваться вашей программой,и обработать их.Например,мызнаем, что если имяфайла не задано, этот кодбудет выдавать исключение IndexOutOfRangeException, аеслизадано недопустимое имяфайла, кодвыдает исключение FileNotFoundException.Ранее вэтой главетакжебыло показано,что припопыткечтениянечитаемого файла CLR выдает UnauthorizedAccessException. Чтобы в приложении по-разному обрабатывались раз- ныевиды исключений,добавьтенесколькоблоков catch в свой код: static void Main(string[] args) { var firstLine = "No first line was read"; try { var lines = File.ReadAllLines(args[0]); firstLine = (lines.Length > 0) ? lines[0] : "The file was empty"; } catch (IndexOutOfRangeException) { Console.Error.WriteLine("Please specify a filename."); } catch (FileNotFoundException) { Console.Error.WriteLine("Unable to find file: {0}", args[0]); } catch (UnauthorizedAccessException ex) { Console.Error.WriteLine("File {0} could not be accessed: {1}", args[0], ex.Message); } fin al ly { Console.WriteLine(firstLine); } } Теперь ваше приложение выводит разные сообщения об ошибках в зависимости от того, какое исключение было обработано программой. Обратитевнимание на то, что впервых двух блоках catch неуказано имя переменной (например, ex, как в третьемблоке).Имя переменной не- обходимозадавать только втомслучае, если высобираетесь работать с объектомException. Можноли предотвратить какие- либо из этих иск лючений (вместо того, чтобы обрабатывать их)? Мозговой штурм Только в этом блоке catch задается имя переменной для объекта Exception. О б р а ботч и к и с к л ю че н и ясо - де р ж и тт р и б л о к а c a tc h д л я об р а бот к и т р ех р а з н ы хт и п ов и с к л ю че н и й .
678 глава 12 В:Выходит, каждый раз, когда в моей программе возникает исключение, она прервет работу, если только я не напишу код для перехвата этого исключения. Что же в этом хорошего? О:Одноизглавныхпреимуществ исключе- ний — то, чтоони не дают пройти мимо про- блемы. Вбольших исложныхприложениях трудноследить за всеми объектами, с кото- рыми работает программа.Исключенияпри- влекают внимание кпроблемам ипомогают понять причины их возникновения,чтобывы всегда знали, соответствует ли поведение пр огр аммы вашим ожида ниям. Появлениеисключениявпрограмме преду- преждает:что-тоидет нетак, как планирова- лось. Может быть, ссылка указывает не на тот объект, на который нужно, или пользо- ватель ввелсовсем нетозначение,которое требовалось, или даже файл, с которым ведетсяработа,вдругсталнедоступен. Если в программе происходит нечто подобное, а вы об этом даже не знаете, скорее всего, результат работы программы изменится, аее поведениес этогомоментабудетсильно отличаться от того, чего вы ожидали при на пис ании прог рамм ы. Атеперь представьте,чтовы дажене знаете обовсех этих ошибках. Пользователь вводит некорректныеданныеиначинает жаловаться, чтоприложениенестабильно. Вотпочему так хорошо,что исключения прерываютработу вашей программы.Онизаставляютвасразо- братьсяспроблемой на том этапе, когдаее еще можно решить легко и безболезненно. В:Напомните, для чего мне использо- вать объект Exception? О: Объект Exception содержит информа- цию о том, что пошло не так. Вы можете воспользоваться типом объекта исключения дляопределения того, какая проблема воз- никла в программе, и написать обработчик исключения, который сохранит работоспо- собность программы. В: Чем обработанное исключение от- личается от необработанного? О: При появлении исключений исполни- тельная среда начинает искать вкодеблок catch, обрабатывающий это исключение. Еслитакойблокприсутствует, блок catch выполнит все действия, предусмотренные вами для этого конкретного исключения. Так как вы заранее написали блок catch для такого исключения, оно считается об- р абот анны м. Еслижеисполнительная среда не находит блок catch, программапростопрекращает работу и выдает сообщение об ошибке. В этомслучаеречь идет онеобработанном исключении. В: Что происходит, если в условии catch не указан конкретный тип исклю- чения? О: Это называется универсальным ис- ключением. Такой блок catch перехва- тывает любые исключения, выдаваемые блоком try. Таким образом, если вам не нужно объявлять переменную для исполь- зования объекта Exception, простейший универсальный обработчик выглядит так: catch { // Обработка исключения } В:Зачем указывать в блоке catch тип обрабатываемого исключения?Развене безопаснее использовать универсаль- ный код? О: Старайтесь как можно реже пере- хватывать Exception. Знаете старую поговорку, что щепотка профилактики луч- ше горсти лекарств?Это правило относит- ся и к исключениям. Попытка обработать все исключения сразу обычно является признаком плохого стиля программирова- ния. Например, лучше воспользоваться методом File.Exists для проверки наличия файла, а не обрабатывать исключение FileNotFoundException. Хотя бывают ис- ключения, которые попросту неизбежны, большинство из них можно предотвратить. В:Но если блок catch без заданного исключения может обрабатывать любые исключения, зачем указывать конкрет- ный тип? О:Несуществуетуниверсального способа обр абот ки всех иск лю ч ений. Нап ример, дл я у с тра нения проб лемы с деле нием на ноль в блокеcatch нужноизменить значенияне- которых переменных исохранить некоторые данные. А чтобы обработать исключение null-ссылки,возможно, вампридется создать но вый эк зем пляр. необработанные исключения делают ваш код нестабильным Необработанное исключение может привести к непредсказуемому поведению программы. Вот почему программа прерывает работу при возникновении исключения. часто Зада вае мые вопросы
дальше 4 679 обработка исключений У бассейна Ваша задача — взять фраг- менты кода из бассейна иразместить ихв пустых строках программы. Лю- бой фрагмент можно ис- пользовать несколько раз, использовать все фрагменты не обязатель- но. Ваша цель — добиться того,чтобыпрограмма вывела приведенныйнижерезультат. Каждый фраг- мент можно ис- пользовать не- сколькораз! using Syst em.IO ; class Program { public static void Main() { Kan garoo joey = ne w Kan garoo (); int koala = joey.Wombat( j oey.W ombat (joey .Womb at(1))); try { Cons ole.W riteL ine(( 15 / koala ) + " eggs per pound"); } catch ( DivideByZeroException ) { Cons ole.W riteL ine(" G'Day Mate !"); } } } Except i on IOExcep t io n NullPointerException Di vid eByZ ero Except i on In vali dC ast Except i on OutOfMemoryException dingo wall aby koal a cr oc platypus ef i fs int j ++ -= += == != FileInfo File Directory St ream F il eSt ream Результат: G'day Mate! try cat ch finally return class Kangaroo { FileStream fs; int croc; int dingo = 0; public int Wombat(int wallaby) { dingo ++; try { if( wallaby >0){ fs = Fi le.Op enWri te("w obbie gong" ); croc = 0; }elseif( wallaby <0){ croc = 3; }else{ fs = File .OpenR ead(" wobbi egong "); croc = 1; } } catch (IOE xcept ion) { croc = -3; } catch { croc = 4; } final ly { if ( dingo >2){ cr oc -= dingo ; } } return croc ; } }
680 глава 12 using System.IO; class Program { public static void Main() { Kangaroo joey = new Kangaroo(); int koala = joey.Wombat( joey.Wombat(joey.Wombat(1))); try { Console.WriteLine((15 / koala) + " eggs per pound"); } catch ( DivideByZeroException ) { Console.WriteLine("G'Day Mate!"); } } } class Kangaroo { FileStream fs; int croc; int dingo = 0; public int Wombat(int wallaby) { d ingo ++; try { if( wallaby >0){ fs = File.OpenWrite("wobbiegong"); croc = 0; }elseif( wallaby <0){ croc = 3; }else{ fs = File .OpenRead("wobbiegong"); croc = 1; } } catch (IOException) { croc = -3; } catch { croc = 4; } finally { if ( dingo >2){ cro c -= dingo; } } return croc ; } } У бассейна. Решение всплывающие исключения и повторная выдача Блок catch перехваты- вает только исключе- ния с делением на нуль. На то, что это FileStream, указыва- ет наличие метода OpenRead и выдача ис- ключения IOException. Вы уже знаете, что файлы всегда не- обходимо закрывать при завершении работы с ними. Если этого не сделать, файл останется заблокированным и при попытке его повторного открытия произойдет исключение IOException. Код открывает файл с именем «wobbiegong» и оставляет его откры- тым при первом вызове. Позднее он открывает файл снова. Но файл ни- где не закрывается, в ре- зультате чего выдается исключение IOException. Метод joeyWombat вызыва- ется три раза, на третий раз он возвращает 0. Это приводит к тому, что WriteLine выдает исключе- ние DivideByZeroException. Не забывайте: универсаль- ного перехвата исключений следует избегать. Также следует избегать того, что нам пришлось сделать, чтобы головоломка оста- валась интересной, — на - пример, бессодержательных имен переменных.
дальше 4 681 обработка исключений Уберите небезопасный код из конструктора! К настоящему моменту вы уже знаете, что кон - структор не имеет возвращаемого значения, даже void. Дело в том, что конструктор ничего не возвращает. Его един- ственной целью является инициализация объекта, и это создает про- блемы для обработки исключений внутри конструктора. Если исклю- чение выдается в конструкторе, команда, которая пытается создать экземпляр класса, этот экземпляр не получит. Подсказка для карьерного роста: на многих собесе- дованиях на вакан- сии по програм- мированию на C# встречается вопрос о том, как обрабатывать исключения в кон- структоре. Выпродолжаете говорить о небезопасном коде, но разве безопасно оставлять исключение необработанным? Зачем писать обработку исключений, которая не обрабатывает все возможные типы исключений? Необработанные исключения всплывают. Хотитеверьте, хотитенет,но иногдаоставлять исключениянеобработанными дажеполезно. Реальныепрограммы содержатсложную логику,ичастобывает трудно правильно восстановить работоспособность программы, когда что-то пошло не так, особенно если проблема происходит на нижних уровнях про- граммы. Обрабатывая только конкретные исключения и избегаяуниверсаль- ныхобработчиков, выпозволяетенепредвиденнымисключениямвсплывать: вместо того чтобы обрабатываться в текущем методе, они перехватываются следующейкомандойвстеке вызовов.Есливыбудете обрабатывать ожидаемые исключения ипозволитенеобработанным исключениям всплывать,это будет способствовать построениюболее надежныхприложений. Иногда бывает полезно заново выдать исключение;это означает, что исключе- ние обрабатываетсявметоде, нобудет всплыватьдокоманды, изкоторойбыло вызвано. Все,что необходимо для повторнойвыдачиисключений, — вызвать throw; внутриблокаcatch.Перехваченноеисключениенемедленновсплывет: try { // код, который может выдать исключение } catch (DivideByZeroException d) { Console.Error.WriteLine($"Got an error: {d.Message}"); throw; } Команда throw заставляет ис- ключение DivideByZeroException всплыть до кода, вызвавшего этот блок try/catch. Будьте осторожны!
682 глава 12 Использование исключения, подходящего для конкретной ситуации При использовании IDE для генерирования метода добавляется код следующего вида: private void MyGeneratedMethod() { throw new NotImplementedException(); } Исключение NotImplementedException используется каждый раз, когда в программе вызывается не- реализованная операция или метод. Оно прекрасно подходит для временных реализаций — при виде этого исключения вы знаете, что есть код, который необходимо написать. Это лишь одно из многих исключений, предоставляемых.NET. Выбор подходящего исключения упроститчтение вашего кода, а обработка исключенийстанетболее ясной и надежной. Например,код в методе, проверяющем его параметры, может выдать исключение ArgumentException, укоторогоимеется перегруженныйконструктор с параметром, сообщающим, какой аргумент создал проблему.Вспомните классGuy из главы 3: его метод ReceiveCash проверял параметр amount,чтобыубедитьсявтом,что он получает положительную сумму.В такойситуацииуместновыдать исключение ArgumentException: publ ic vo id Re ceive Cash( int a mount ) { if (amount <= 0) th row n ew Ar gumen tExce ption ($"Mu st rec eive a pos itive valu e", " amoun t"); Cash += am ount; } Просмотритесписок исключений,являющихся частью .NET API, — любые изэтих исключенийвысмо- жете выдать в своем коде: https://docs.microsoft.com/en-us/dotnet/api/system.systemexception. Перехват специализированных исключений, расширяющих System.Exception Иногда программа должна выдавать исключение из-за специального условия, которое может возникнуть привыполнении. Вернемся кклассуGuyиз главы3.Допустим,вы используете его в приложении, кото- роеполностью зависитоттого, что Guy всегда имеет положительную сумму денег. Вы можете добавить специализированное исключение,расширяющее System.Exception: clas s Out OfCas hExce ption : Sy stem. Excep tion { publi c Out OfCas hExce ption (stri ng me ssage) : ba se(me ssage ) { } } Теперь можно выдатьэтоновое исключениеи перехватить еготочнотакже, как и любое другоеисключение: clas s Guy { publi c str ing N ame; publi c int Cash ; publi c int Give Cash( int a mount ) { i f (Ca sh <= 0) t hrow new O utOfC ashExc eptio n($"{ Name} ran out o f cas h"); ... создание собственных исключений Имя недействительного аргумента пере- дается конструктору ArgumentException. Специализированное исклю- чение OutOfCashException расширяет базовый кон- структор System.Exception параметром message. Теперь Guy выдает специализированное исключение, и любой метод, который вы- зывает GiveCash, может обработать это исключение в собственном блоке try/catch.
дальше 4 683 обработка исключений class ExTestDrive { public static void Zero(string test) { Con sole. Write ("r"); } } try { Console.Write("t"); Console.Write("o"); DoRisky(test); } Console.Write("a "); } finally { Console.Write("w "); Console.Write("s"); } static void DoRisky(String t) { Console.Write("h"); if (t == "yes") { } } catch (MyException) { throw new MyException(); class Program { public static void Main(string[] args) { Console.Write("when it "); ExTestDrive.Zero("yes"); Console.Write(" it "); ExTestDrive.Zero("no"); Console.WriteLine(" ." ); } } class MyException : Exception { } Развлечения с магнитами Разместите магниты так, чтобы программа выводила наконсоль следующийрезультат: when it thaws it throws.
684 глава 12 class ExTestDrive { public static void Zero(string test) { Console.Write("r"); } } try { Console.Write("t "); Console.Write("o "); DoRisky(test); } Console.Write("a "); } finally { Console.Write("w "); Console.Write("s "); } static void DoRisky(String t) { Console.Write("h "); if (t == "yes") { } class Program { public static void Main(string[] args) { Console.Write("when it "); ExTestDrive.Zero("yes"); Console.Write(" it "); ExTestDrive.Zero("no "); Console.WriteLine(". "); } } class MyException : Exception { } команда using создает обработчик исключения } catch (MyException) { throw new MyException(); Эта строка определя- ет специализированное исключение с именем MyException, которое перехватывает в блоке catch в коде. Метод DoRisky выдает исключение только при передаче строки "yes". Эта строка выпол- няется только в том случае, если DoRisky не выдаст исключение. Метод Zero выводит либо «thaws», либо «throws» в зависимости от того, было ли передано в пара- метре test значение "yes" или что-то другое. Блок finally гарантирует, что метод всегда выве- дет"w", а "s" выводится вне обработчика исключе- ний (и поэтому выводится всегда). Развлечения с магнитами. Решение Разместите магниты так, чтобы программа выводила наконсоль следующийрезультат: when it thaws it throws.
дальше 4 685 обработка исключений Избегайте лишних исключений... Всегда ис- пользуйте блок USING при работе с потоком! Или все, что угод но, ч то реализует IDisposable. IDisposable использует try/finally для обеспечения вызова метода Dispose Помните, какмы анализировали этот код из главы 10? using System.IO; using System.Text; class Program { static void Main(string[] args) { using (var ms = new MemoryStream()) { using (var sw = new StreamWriter(ms)) { sw.WriteLine("The value is {0:0.00}", 123.45678); } Console.WriteLine(Encoding.UTF8 .GetString(ms.ToArray())); } } } Мыразбирались в том, как работают команды using, и в данном случае нам пришлось вложить одну команду using в другую, чтобы гарантировать, что экземпляр StreamWriter будет освобожден ранее MemoryStream.Мыпоступили так,потомучтооба класса, StreamWriter и MemoryStream, реализуют интер- фейсIDisposable,и включили вызовметодаClose вихметодыDispose.Каждаякомандаusingгарантирует, что методDisposeбудет вызван в конце ее блока; этогарантирует, что потоки всегда будут закрываться. Теперь вызнаете, какработаетобработка исключений, исможете понять, какработает командаusing. Команда usingявляетсяпримеромсинтаксического сахара:C#предоставляетудобную сокращенную запись, котораяупрощаетчтениекода: using (var sw = new StreamWriter(ms)) { sw.WriteLine("The value is {0:0.00}", 123.45678); } Компилятор C# генерирует откомпилированный код, который выглядит (приблизительно) так: try { var sw = new StreamWriter(ms)) sw.WriteLine("The value is {0:0.00}", 123.45678); } finally { sw.Dispose(); } Размещениекоманды Disposeвблокеfinallyгарантирует,что онбудет выполнен влюбом случае, дажепри возникновении исключения. IDisposable – эффек- тивный инст ру мен т для пр ед от- вращения исключ ен ий . Эта команда using перемести- лась вовнутрь, чтобы метод StreamWriter.Close вызывался перед методом MemoryStream.Close. За сценой
686 глава 12 Если try/catch — такая замечательная штука, почему быIDE не заключать в них все подряд? Тогда нам не пришлось бы писать все эти блоки try/catch самостоятельно. Не так ли? Всегда лучше определить самый точный обработчик исключений из всех возможных. Обработка исключений не сводится к простой выдаче сооб- щения об ошибке. Иногда разные исключения должны об- рабатываться по-разному — например, программа HexDump обрабатывала исключение FileNotFoundException не так, как исключение UnauthorizedAccessException. В планировании для обработкиисключенийвсегда учитываютсянепредвиденные ситуации. Иногдатакие ситуацииможнопредотвратить, иногда ониобрабатываются,а иногдаисключения должны всплывать. Из всегосказанногоследует вынести важныйурок:не существу- ет единого подхода к обработке непредвиденных условий «на все случаи жизни»; именно по этой причинеIDE не пытается упаковывать всеподряд в блокиtry/catch. Вот почему суще- ствует так мно- го разных классов, производных от Exception. И воз- можно, вам тоже захочется написать собственные клас- сы, наследующие от Exception. Фильтры исключений повышают точность обработки исключений Допустим,вы строите игру, действие которой происходит в классической мафиозной атмосфере1930-х годов. Класс LoanShark(Ростовщик) должен собирать деньги от экземпляровGuy при помощи метода Guy.GiveCash и обрабатывать исключенияOutOfCashException (Денег нет)классическимурокомпоправилам мафии. Каждыйростовщик знает золотое правило: не пытайтесь выбивать деньги из босса мафии. В таких ситуациях вам могут пригодиться фильтры исключе- ний.Фильтр исключенияиспользует ключевое слово when, чтобы приказать обработчику исключенияперехватывать исключение только в конкретныхусловиях. Пример использованияфильтра исключений: try { loanShark += guy.GiveCash(amount); emergencyReserves -= amount; } catch (OutOfCashException) when (guy.Name == "Al Capone") { Console.WriteLine("Don't mess with the mafia boss"); loanShark += amount; } catch (OutOfCashException ex) { Console.Error.WriteLine($"Time to teach {guy.Name} a lesson: {ex.Message}"); } О б ъ е к т Loa n S h a r k Лучше не серди нас, парень. Этот фильтр исключений перехва- тывает OutOfCashException только в том случае, если guy.Name содер- жит "Al Capone", в противном случае исключение пройдет насквозь к следующему блоку catch. фильтры исключений обрабатывают конкретные условия
дальше 4 687 обработка исключений В:Мневсе еще непонятно, когда пере- хватывать исключения, когда предотвра- щать и когдаразрешать им всплывать. О: Не существует единственно правиль- ного ответа или правила, которое можно соблюдать вкаждой ситуации, — э т о вс егда зависит от того, что долженделать ваш код. Впрочем,и это утверждениеработаетне на стопроцентов. Существует правило:лучше предотвращать все исключения, если это возможно. Не всегдаможнопредвидеть все неожи данн ост и, о соб енно ког да вы име ете делос вводом от пользователей илиреше- ниями, которые они принимают. Решения относительно того, разрешать ли исключениям всплывать или обрабатыватьих в классе, часто сводятся кразделению обязан- ностей. Насколькоразумно классу знать о су- ществованииопределенного исключения?Это зависит от того, что делает класс. К счастью, средстварефакторинга IDE всегда помогут вам изменить этот код, если вы решите, что некоторыеисключения лучшепередавать на верхниеуровни, чем перехватывать. В: Объясните, что такое «синтаксиче- ский сахар»? О: Ког да ра зработч ики уп оминаю т «с интак - сический сахар», они обычно имеют в виду, что язык программирования предоставляет удобную и понятную сокращенную запись длякода, который обычно бывает громозд- ким и неудобным. Термином «синтаксис» обозначаются ключевые словаC# и прави- ла, которые имиуправляют. Команда using официальноявляется частью синтаксисаC# иподчиняется следующему правилу: за ним должноследовать объявлениепеременной, которое создает экземпляр типа, реализую- щегоIDisposable, азатемследуетблок кода. Под «сахаром»имеетсяв видутотфакт, что компилятор C# преобразует сокращенную запись в намногоболее громоздкий вариант, если бы ее пришлось записывать вручную. В:Возможно ли использовать объект с командой using, если он не реализует IDisposable? О:Нет. Командой using можно создавать только объекты, реализующие IDisposable, потому чтоонибылиразработаныв расчете друг на друга. Добавление команды using аналогично созданию нового экземпляра класса, не считая того, что в конце блока всегда вызывается метод Dispose. Вот по- чему классдолженреализовать интерфейс Disposable. В:Вблокеusing можно размещать лю- бые команды? О:Разумеется. Командаusing всеголишь га рантиру ет у нич тож ение л юбог о с озд анного вами объекта, но что вы делаете с этими объектами, зависит исключительно от вас . Можно даже создать объект при помощи команды using и не использовать его вну- три блока(конечно, это было бы довольно бессмысленно,так что мы так поступать не рек оменд у ем). В: Можно ли вызвать Dispose вне команды using? О:Да.Насамомделеиспользовать коман- дуusing вообщене обязательно.Вы можете самостоятельновызвать Dispose,завершив работу с объектом. Методосвободитуказан- ныересурсы,какиприручном вызове метода Closeдля потока(такделалось в главе10). Команда usingвсеголишь облегчаетчтение и по нимани е ко да, а т акже пред отвра щает проблемы, которые могут возникнуть, если объект небудет своевременноудален. В:Таккаккоманда using, по сути, генери- рует блокtry/catch, который вызывает метод Dispose, можно ли выполнять об- работкуисключений внутриблокаusing? О: Да. Она работает точно так же, как вложенные блоки try/catch, которые использовались ранее в главе с методом G etInpu tStre am. В:Выупомянулиблок try/finally. Оз- начает ли это, что командыtry иfinally могут фигурировать без catch? О: Да! Блок try можно скомбинировать непосредственно с блоком finally, как пока зано ниже: try { DoSomethingRisky(); SomethingElseRisky(); } finally { AlwaysExecuteThis(); } Если методDoSomethingRisky() выдаст ис- ключение, немедленно будет выполнен блокfinally. В: Метод Dispose работает только с файлами и потоками? О: Нет. Многие классы реализуют ин- терфейс IDisposable, и приработе с ними всегда нужно использовать команду using. Если вы напишете класс, который нужно утилизировать определенным способом, также реализуйтеинтерфейс IDisposable. Не существует единственно правильного подхода к планированию непредвиденного. часто Зада вае мые вопросы
688 глава 12 Наихудший блок catch: универсальный перехват с комментариями Блок catch позволяет программе продолжить работу, если разработчикпредусмотрелтакую возможность. Появившееся исключениеобрабатывается,ивместо аварийной остановки исообщенияобошибке программа продолжает работать. Как нистранно,это невсегда хорошо. Рассмотрим класс Calculator,с которым явно что-то нетак. Чтожепроисходит? Исключения нужно обрабатывать, а не скрывать class Calculator { public void Divide(int dividend, int divisor) { try { this.quotient = dividend / divisor; } catch { // TODO: придумать, как предотвратить ввод // пользователем нулевого делителя. } } } Тотфакт,что программа продолжает работу,ещенеозначает, что исключение было обработано. Приведенный выше код не будет аварийно остановлен... по крайней мере, не в методе Divide. Но что, если этот метод вызывается другимметодом, который пытается вывестирезультат?Если делитель равен нулю, метод, скорее всего, вернет неправильное(и неожи- да нно е) зна ч ение. Нужнонепростодобавить комментарий искрытьисключение, а обработатьисключение. Даже есливыне можете справить- ся с проблемой сейчас, не оставляйтеблок catch пустым или закомментированным!Это лишьусложнитисправлениеошиб- ки в будущем. Лучше пусть программа выдаст сообщение об исключении, — так вам будетпрощеразобраться,что именно работаетне так. Проблема: если делитель равен нулю, выдается исключение DivdeByZeroException. не скрывайте исключения Программист подумал, что сможет скрыть исключения при помощи пустого блока catch, но он всего лишь создал проблему тому, кто позднее будет разбираться с ошибкой. Помните, что если исключе - ние не обрабатывается, оно поднимается вверх в стеке вызовов. В некоторых ситу- ациях это становится со- вершенно нормальным вари- антом обработки исключения. Предоставить исключению возможность всплывать на- верх почти всегда лучше, чем пустой блок catch.
дальше 4 689 обработка исключений class Calculator { public void Divide(int dividend, int divisor) { try { this.quotient = dividend / divisor; } catch (Exception ex) { using (StreamWriter sw = new StreamWriter(@"C:\Logs\errors.txt"); { sw.WriteLine(ex.getMessage()); } } } } Я понял! Мы используем обработку исключений,чтобыпометить проблемную область. Проблема нику- да не исчезла, но по крайней мере стало ясно, где она возникла. Однако не лучше ли было ра- зобраться, почему ваш метод Divide вызывается с нуле- вым знаменателем, и устранить эту возможность? Обработка исключения далеко не всегда означает УСТРАНЕНИЕ исключения. Плохо, когда в вашей программе происходит фатальный сбой. Ноеще хуже, есливыне понимаете, почемупрограмма аварийно завершается или что она делает с данными пользователя. По- этому всегда нужно обрабатывать ошибки, которые вы можете предсказать, и записывать в журналинформацию об ошибках, с которыми вы пока не можетебороться. Хотя журнальнаяин- формация помогает в отслеживании проблем, лучше с самого началапредотвратить этипроблемы— это более надежное идаль- но видно е р ешен ие. Временные решения допустимы (но только временно) Иногда,столкнувшись спроблемой,вы понимаете,что нужно что-то делать,но незнаете,что именно. В этом случае имеет смысл зарегистрироватьошибкувжурна- ле,снабдив ее примечанием. Это нетак хорошо, как обработка исключения,но лучше,чем ничего. Временное решение для калькулятора может выгля- деть так: ...к сожалению,вреальной жизни «временные» ре- шения зачастую стано- вятся постоянными. Остановитесь и проанализи- руйте блок catch. Что про- из о й дет, если Stre amWriter не сможет сделать запись в папку C:\Logs\? Для снижения риска можно вложить внутрь еще один блок try/catch. Можете предложить лучший способ?
690 глава 12 ключевые моменты ¢ Любая команда может выдать исключение, если что-то пойдет не так вовремя выполнения. ¢ Для обработки исключений используется блок try/ catch. Необработанные исключения приводят к аварийному завершению программы и появлению окна с информацией об ошибке. ¢ При возникновении исключения в блоке кода, сле- дующем за ключевым словом try, управление не- медленно передается первой команде обработчика исключения, т . е . блока кода после catch. ¢ ОбъектException содержит информацию о перехва- ченном исключении. Объявив переменную Exception в команде catch, вы получаете доступ к информации о любом исключении, выданном в блоке try: try { // statements that might // throw exceptions } catch (IOException ex) { // if an exception is thrown, // ex has information about it } ¢ Существуют различные виды исключений, кото- рые могут перехватываться в программе. Каждому виду соответствует объект, расширяющий System. Exception. ¢ Старайтесь избегать универсальных обработ- чиков исключений, перехватывающих Exception. Обрабатывайте исключения конкретных типов. ¢ Блокfinally после обработчиков исключений вы- полняется всегда, независимо от того, выдается ис- ключение или нет. ¢ Каждой команде try может соответствовать несколько блоков catch: try{...} catch (Nul lRefe rence Excep tion ex) { // Эти кома нды срабат ывают при выдаче // искл ючени я Nul lRefe rence Excep tion } catch (Ove rflow Excep tion ex) { ... } catch (Fil eNotF oundE xcept ion) { ... } catch (Arg ument Excep tion) { .. . } ¢ Ваш код может выдаватьисключения командойthrow: throw new Exception("Exception message"); ¢ Ваш код также может повторно выдать перехваченное исключение командой throw, но эта возможность работает только внутри блока catch. Повторная вы- дача исключениясохраняет состояние стека вызовов. ¢ Чтобы определить пользовательское исключе- ние, добавьте класс, расширяющий базовый класс System.Exception: class CustomException : Exception { } ¢ В большинстве случаев хватает исключений, встро- енных в .NET, таких как ArgumentException. Ис- пользуяразные типы исключений, вы предоставляете пользователю больше информации длядиагностики. ¢ Фильтр исключений использует ключевое слово when, чтобы приказать обработчику исключений пе- рехватывать исключение только при определенных условиях. ¢ Команда using представляет собой «синтаксический сахар», который заставляет компилятор C# сгенери- ровать эквивалент блока finally с вызовом мето- да Dispose. КЛЮЧЕВЫЕ МОМЕНТЫ
Head First C# Лабораторный курс Unity No 6 691 Лабораторный курс Unity No6 В последней лабораторной работе Unity была созда- на сцена с полом (плоскость) и игроком (сфера с ци- линдром). При этом использовался объект NavMesh, NavMesh Agent и отслеживание лучей, чтобы игрок следовал за щелчками в сцене. Продолжим работу с той точки, в которой прервалась последняя лабораторная работа Unity. Цель этих ла- бораторных работ — помочь вам в изучении систе- мы поиска путей и навигации: сложной системы на базе искусственного интеллекта, которая позволя- ет создавать персонажей, способных находить путь в построенных вами игровых мирах. В этой работе мы воспользуемся навигационной системой Unity, чтобы объекты GameObject сами перемещались по сцене. Попутно вы освоите ряд полезных инструментов: мы создадим более сложную сцену и заранее сгенерируем сетку NavMesh, которая позволяет объекту переме- щаться по сцене, построим статические и перемещаю- щиеся препятствия и, что самое важное, — получим опыт написания кода C#. Лабораторный курс Unity No 6 Перемещение по сцене https://github.com/head-first-csharp/fourth-edition
692 глава 12 Лабораторный курс Unity No6 В: Впоследней лабораторной работеUnityбыломногоновых концепций.Нельзя ли снова пробежаться по ним,чтобы ябыл уверен, что ничего не забыл? О: Конеч но. С цена Uni t y, с озданн ая в п осле дней лабор аторной рабо - те,состоит изчетырех частей.Разобраться в том,как онисочетаются друг с другом, не так просто, поэтому перечислим их поочередно: 1.СеткаNavMesh,определяющая все «проходимые»места,по кото - рымперсонаж может перемещаться в сцене. Чтобы создать ее, мы назначилипол проходимойповерхностью, азатемобработалисетку. 2. NavMesh Agent — компонент , который может «взять под кон- троль» объект GameObject и перемещать его по сетке NavMesh простымвызовом методаSetDestination. Мыдобавили его к объекту GameObjectPlayer. 3.Метод ScreenPointToRay камеры создает луч,которыйпроходит через точку на экране. Мы добавили в метод Update код, который проверяет,нажата ликнопкамыши в настоящиймомент.Есликнопка нажата, тотекущая позиция мышииспользуетсядля вычисления луча. 4.Отслеживаниелучей — механизм,прокладывающий луч в сцене. Unity предоставляет метод Physics.Raycast, который получает луч, прокладываетегонаопределенноерасстояние, иеслилучстолкнулся с каким-то объектом, сообщает вам об этом. В:Как жеэтичастиработают другсдругом? О: Каждыйраз, когда вы пытаетесь определить, как разныечасти системы взаимодействуют друг с другом, начинать следует с по- нимания общей цели. В данном случае цель заключается в том, чтобы игрок могщелкнуть в любой точке пола, а объектGameObject автоматически переместился в выбранную точку. Разобьем ее на отдельные шаги. Код должен: • Обнаружить, что пользователь щелкнул кнопкой мыши. Ваш код использует Input.GetMouseButtonDown для обнаружения щелчка кнопки мыши. • Определить, какой точке сцены соответствует точкащелчка. Дляпрокладки луча и определения точки сцены, в которой был сделан щелчок, используются методы Camera.ScreenPointToRay и Physics.Raycast. • Приказать NavMesh Agent установить эту точку в качестве конечной точки для перемещения. Метод NavMeshAgent.SetDestination заставляет агента вычислить новый путь и начать движение к новой точке. МетодMoveToClick представляет собой адаптированную версию кода из страницы руководства Unity для метода NavMeshAgent. SetDestination.Выделитенемного времени ипрочитайте ее —выбе- ритекомандуHelp>>ScriptingReferenceиз главногоменю,а затем проведите поиск по имени NavMeshAgent.SetDestination. Продолжим с того места, на котором прервалась последняя лабораторная работа Unity Впоследней лабораторнойработеUnityмысоздалиигрока со сферической головой и цилиндрическим телом. Затембыл добавлен компонентNavMeshAgent для перемещения игрока по сцене; для опре- деления точкипола,на которойбыл сделан щелчок,используется отслеживание лучей.Мы добавим всцену объектыGameObject,включаялестницыипрепятствия,чтобывы видели,какискусственный интеллектнавигации Unity справляется с ними. Затем мы добавим движущееся препятствие, чтобы компоненту NavMeshAgent пришлось действительно постараться. ОткройтепроектUnity, сохраненныйв кон- це последней лабораторной работы Unity. Если вы сохраняли лабораторные работы Unity, выполняя их одну за другой, скорее всего, вы сможете с ходу взяться за работу! Аеслинет, потратьте несколькоминут исно- вапросмотрите последнюю лабораторнуюра- ботуUnity—а такжекод, написанный длянее. Сценарий MoveToClick при помощи механизма отслеживания лучей находит точку пола, на которой щелкнул пользователь, и исполь- зует ее для выбора итоговой точки перемещения NavMesh Agent. Сетка NavMesh определяет область, в которой выполняет- ся навигация, а компонент NavMesh Agent пере- мещает по ней объект GameObject. Если вы читаете нашу книгу, потому что собираетесь стать профессиональ- ным разработчиком, умение читать и проводить рефакторинг кода старых проектов является очень важным навы- ком — и не только для разработки игр! часто Задава ем ые вопросы
Head First C# Лабораторный курс Unity No 6 693 Лабораторный курс Unity No6 Добавление платформы в сцену Немного поэкспериментируем с системой навигации Unity. Для этого мы до- бавимновыеобъектыGameObjectипостроим изнихплатформу слестницей, наклонной плоскостью ипрепятствием. Вот какэто выглядит: Чтобывамбыло немного проще разобратьсявпроисходящем, мыпереключим- ся визометрическийрежим,вкоторомперспектива отсутствует. В режиме перспективыобъекты, находящиесядальше отзрителя,кажутсямаленькими, тогда какблизкиеобъекты —большими.В изометрическомрежимеобъекты всегда имеютодинаковыйразмер независимо отихрасстояния до камеры. Добавьте в сцену 10 объектов GameObject. Создайте новый материал с именем Platform в папкеMaterials с цветом альбедо CC472Fи добавьтеего ко всем объектам GameObject, за исключением объекта Obstacle, который будет использовать новый материал с именем 8 Ball с картой 8 Ball Texture из первой лабораторнойработыUnity. В следующей таблице перечислены имена,типы и позицииобъектов: Na me Typ e Position Rotation Scale Stair Cube (15,0.25,5) (0,0,0) (1,0.5,5) Stair Cube (14, 0 .5, 5) (0,0,0) (1,1,5) Stair Cube (13,0.75,5) (0,0,0) (1,1.5,5) Stair Cube (12,1,5) (0,0,0) (1,2,5) Stair Cube (11, 1.25, 5) (0,0,0) (1,2.5,5) Stair Cube (10, 1 .5, 5) (0,0,0) (1,3,5) Wide stair Cube (8.5,1.75,5) (0,0,0) (2,3.5,5) Platform Cube (0.75 , 3.75, 5) (0,0,0) (15,0.5,5) Obstacle Capsule (1, 3.75, 5) (0,0,90) (2.5,2.5,0.75) Ra mp Cube (–5.75,1.75,0.75) (–46,0,0) (2,0.25,6) Когда вы запустите Unity, в метке ( )под ма- нипуляторомScene выво- дитсяимя представления. Трилинии( ) показывают, что манипулятор находит- ся в режиме перспективы. Щелкните на конусах, чтобы переключиться в режим задней проекции ( ). Еслищелкнуть на линиях, они заменятся тремя параллельными линиями ( ), это означает, что сцена переключилась в изометрическийрежим. Иногда проще увидеть, что про- исходит в сцене, при переключе- нии в изометри- ческий режим. Если вы пере- станете ориенти- роваться в пред- ставлении, макет все гда мож но сбросить к исходному состоя н и ю. Объект Capsule на- поминает цилиндр с полусфера- ми на обоих тор ца х. Попро- буйте со- зда ть первую ступеньку, а затем продубли- ровать ее 5разииз- менить ее настройки.
694 глава 12 Лабораторный курс Unity No6 Изменение настроек предварительного построения Используйте комбинацию Shift+щелчок на всех новых объектах GameObject, добавленных в сцену, затем снимите выделение с объекта Obstacle, щелкнув на нем с нажатой клавишей Control(иликлавишейCommand на Mac). Перейдите в окно Navigation и щелкните на кнопке Object, азатемназначьтеих проходимыми— установитефлажокNavigationStatic ивыберите в списке Navigation Area пунктWalkable. Вы може те вы бра ть ср азу нес кол ько объе ктов GameObject в окне Hierarchy и задать их настройки навигации одновременно. Теперь выполнитете же действия, которые использовались до предварительного построения сеткиNavMesh:щелкните на кнопке Bake в верхней частиокнаNavigation, чтобыпереключить- ся врежим предварительного построения,а затем щелкнитена кнопкеBakeв нижней части. Сработало!Теперь сеткаNavMesh отображаетсяповерх платфор- мы, а вокругпрепятствия имеется свободное пространство. По- пробуйтезапустить игру.Щелкнитена верхнейчасти платформы ипосмотрите,что произойдет. Гмм, что-то не то. Все работает не так, как ожидалось. Когда вы щелкаете на верхней поверхности платформы, игрок перемеща- ется под нее. Если внимательно присмотреться к сеткеNavMesh, отображаемой при просмотре окнаNavigation, мы видим, что лестница и наклонная плоскость не включены в NavMesh.Игрок не может добраться до точки, на которойбыл сделанщелчок, по- этому искусственный интеллектподводитего какможно ближе. При щелчке на верхней по- верхности платформы игрок заходит под нее.
Head First C# Лабораторный курс Unity No 6 695 Лабораторный курс Unity No6 Включение лестницы и наклонной плоскости в NavMesh Искусственный интеллектне можетзаставитьвашегоперсонажа поднятьсяили спуститьсяпо наклонной плоскости илилестнице. Ксчастью,системапоискапутейUnityпозволяетсправитьсяс обоими случаями. Дляэтого достаточно внести несколько небольших изменений в настройки предварительного постро- енияNavMesh.Начнем с лестницы. Вернитесь кокнуBake и обратитевниманиенато, что значение по умолчаниюдляStepHeightравно0.4 .Внимательно присмотритесь кпараметрамступеней вашейлестни- цы— ихвысота составляет0.5единицы.Чтобыприказатьсистеме навигациивключить ступенивысотой 0.5 единицы, изменитезначение вполеStepHeight на 0.5.Выувидите, что изображение ступеней на диаграмме становится выше, а число над ним увеличивается с используемого поумолчанию 0.4до 0.5. Такженеобходимовключитьв NavMesh наклонную плоскость.Присоздании объектовGameObjectдля платформы мы присвоили наклоннойплоскостиугол поворота X,равный –46; это означаетнаклон под углом46 градусов. ПараметрMaxSlopeпо умолчанию равен45, это означает, что в построение будут включаться склоны, наклонные плоскости или другие возвышения с углом, не превышающим 45градусов.ИзменитеMaxSlopeна46 иснова проведитепостроениеNavMesh.На этотразвсетку будут включены наклонная плоскость и ступени. Запустите игру и протестируйте изменения в NavMesh. Еще одно упражнение по программированию для Unity! Ранее мы направили камеру сверху вниз, на - значив ей угол поворота по осиX,равный90.Чтобы игрок мог лучшерассмотреть своего персонажа, было бы неплохо реализовать управление камерой при помощи клавиш со стрелками и колеса мыши. Выуже знаете почти все необходимое — нужно лишьдобавить немного кода. Задача кажется сложной, но она вам по силам! • Создайте новый сценарий с именем MoveCamera и перетащите его на камеру. Он должен содержать поле Transform с именем Player. Перетащите объект GameObjectPlayer из окнаHierarchy на поле Player в окне Inspector. Так как поле имеет тип Transform, операция копирует ссылку на компонент Transform объекта GameObjectPlayer. • Реализуйте вращение камеры вокруг игрока с использованием клавиш со стрелками. Input.GetKey(KeyCode LeftArrow) вернет true, если игрок в настоящее время нажимает клавишу ¥; для проверки других направлений ис- пользуются значения RightArrow, UpArrow и DownArrow. Используйте этот метод так же, как вы использовали Input. GetMouseButtonDown в сценарии MoveToClickдля проверки щелчков мышью. Когда игрок нажимает клавишу, вызо - вите transform.RotateAround для выполнения поворота вокруг позиции игрока. Позиция игрока передается в первом аргументе; используйтеVector3.left, Vector3.right,Vector3.up или Vector3.down как значение второго аргумента, а поле Angle(со значением3F) — как значение третьего аргумента. • Реализуйте изменение масштаба камеры с использованием колеса мыши. Input.GetAxis("Mouse ScrollWheel") возвращает число (обычно вдиапазоне от –0.4 до 0.4), представляющее смещение колеса (или 0, если оно не дви- галось).Добавьте поле float с именем ZoomSpeed,равное 0.25F. Проверьте, сместилось ли колесо мыши . Если оно сместилось, выполните векторные вычисления для масштабирования камеры, умноживtransform.position на (1F+sc rollWheelValue*ZoomSpeed). • Направьте камеру на игрока. Метод transform.LookAt направляет объект GameObject в заданную позицию. Затем сбросьте компонент Transform объекта Main Camera в позицию (0, 1, –1 0) споворотом (0, 0 ,0). Не забывайте: подсмотреть в решение — не значит жульничать! Упражнение
696 глава 12 Лабораторный курс Unity No6 Еще одноупражнение по программированиюдля Unity! Ранее мы направили камеру сверху вниз, назначив ей угол поворота по оси X, равный 90. Чтобы игрок мог лучше рассмотреть своего персонажа, было бы неплохо реализовать управление камерой при помощи клавиш со стрелками и колеса мыши. Вы уже знаете почти все необходимое — нужно лишь добавить не- много кода. Задача кажется сложной, но она вам по силам! p ublic clas s Mov eCame ra : MonoB ehavi our { p ublic Tran sform Play er; public float Angle = 3F; public float ZoomSpeed = 0 .25F; void Update() { var scrol lWhee lValu e = I nput. GetAx is("M ouse Scroll Wheel "); if ( scrol lWhee lValu e != 0) { trans form. posit ion * = (1F + sc rollW heelV alue * Zoom Speed ); } if ( Input .GetK ey(Ke yCode .Righ tArro w)) { trans form. Rotat eArou nd(Pl ayer. posit ion, Vector 3.up, Angl e); } if ( Input .GetK ey(Ke yCode .Left Arrow )) { trans form. Rotat eArou nd(Pl ayer. posit ion, Vector 3.dow n, An gle); } if ( Input .GetK ey(Ke yCode .UpAr row)) { trans form. Rotat eArou nd(Pl ayer. posit ion, Vector 3.rig ht, A ngle) ; } if ( Input .GetK ey(Ke yCode .Down Arrow )) { trans form. Rotat eArou nd(Pl ayer. posit ion, Vector 3.lef t, An gle); } tran sform .Look At(Pl ayer. posit ion); } } Не забудьте перетащить Player на поле компонента сце- нария объекта Main Camera. Попробуйте поэкспериментировать сразными углами и ско- ростями масштабирования. Найдите вариант, который по- кажется вам наиболее подходящим. Перед вами пример того, как простые арифме- тические опера- циис векторами упрощают задачу. Позиция объ- екта GameObject определяется вектором, поэто - му умножение его на 1.02 несколько отдаляет его от нулевой точки, а умножение на 0.98 — прибли- ж ает. Происходит то же, что при использовании transform.RotateAround в двух последних лабо- раторных работах, только вместо Vector3.zero (0, 0, 0) поворот выполняется вокруг игрока. Мы предложили вам создать поле Player с типом Transform. Здесь вы получаете ссылку на компо- нент Transform объекта GameObject Player, так что позиция Player соответствует позиции игрока. Ничего страшного, если ваш код отличается от нашего. У любой задачи по программированию существует множество решений! Главное — не жалейте времени и разберитесь в том, как работает этот код. Используйте клавиши со стрелками для перемещения камеры, чтобы она была направлена на игрока. Вы можетесмотреть прямо сквозь плоскость пола! Вы не забыли сбросить позицию и углы поворота главной камеры? Если забы- ли, начало движения игрока можетбыть немного «дерганым» (из-за способа вы- числения угла в методеCamera.LookAt). Упражнение Реше н ие
Head First C# Лабораторный курс Unity No 6 697 Лабораторный курс Unity No6 Решение проблем с высотой в NavMesh Итак,теперь выможетеуправлять камерой,и мы можем лучшерассмотреть,что происходитподплат- формой, — издесь определенно что-тоне так.Запустите игру, поверните камеруиувеличьте изображение так, чтобы хорошо видеть выступ препятствия под платформой. Щелкните на полу с одной стороны препятствия, затем сдругой.Все выглядит так, словно игрокпроходит прямо сквозь препятствие!Иеще онпроходитчерез конец наклоннойплоскости. При перемещении голова игрока проходит сквозь препятствие. Ноеслизавестиигрока на верхплатформы,онпрекрасно обходитпрепятствие.Что происходит? Внимательноприсмотритесь ко всем частямNavMeshвыше иниже препятствия.Заметиликакие-нибудь отличия? Над платформой вокруг препятствия в NavMesh суще- ствует промежуток, а внизу никаких промежутков нет, поэтому игрок про- сто проходит сквозь препятствие. Вернитесь ктойчастипоследнейлабораторнойработы, вкоторойвынастраиваликомпонентNavMesh Agent, а конкретно к той,в которойHeight присваивалось значение3.Теперь то жесамое необходимо проделать для NavMesh.Вернитесь к настройкамBakeв окнеNavigation, задайтев полеAgent Height значение 3, а затем снова проведитепредварительноепостроение сетки. Темсамымвысоздаете промежуток всеткеNavMeshпод препятствиемирасширяетеэтотпромежуток.Теперь игрок не столкнется ни с препятствием, ни с наклонной плоскостью при перемещении под платформой.
698 глава 12 Лабораторный курс Unity No6 Добавление препятствия в сетку NavMesh Мыужедобавилистатическое препятствиевсередине платформы:для этогобыласозданарастянутаякапсула (Capsule), помеченнаякакнепроходимая,после чего мыпредварительноподготовили объектNavMesh сот- верстием, окружающимпрепятствие, чтобыигрокуприходилось обходить его.Что, если вампонадобится подвижноепрепятствие?Попробуйте переместитьего —сетка NavMesh неизменится!Отверстие вней все еще находитсятам, гдепрепятствиенаходилось,а не там,где ононаходитсявнастоящий момент.Если выснова предварительно построитеNavMesh, тоотверстие простопоявитсяв новой позиции препятствия.Чтобы добавить подвижноепрепятствие,необходимодобавить компонентNavMeshObstacle кобъектуGameObject. Давайтесделаем это. Добавьтев сцену куб(Cube)спозицией(–5.75,1, –1)имасштабом(2,2,0.25).Соз- дайтедлянегоновыйматериал с темно-серым цветом (333333),присвойте новомуобъектуGameObject имяMovingObstacle. Онбудет действовать каксвоегорода «ворота»внижнейчастинаклоннойплоскости: вверхнем положениипрепятствие освобождаетпуть,а внижнем — блокируетего. Это препятствие NavMesh создает движущийся проемв NavMesh, который мешает игроку перемещаться вверхпо наклонной плоскости. Мы добавим сценарий, который позволяет игроку перетаскивать его мышью, чтобы блокировать и освобождать наклонную плоскость. Осталось сделать еще одно. Щелкните на кнопкеAddComponent в нижней части окна Inspector и выбе- ритекомандуNavigation>>NavMeshObstacle,чтобыдобавить компонентNavMeshObstacleкобъекту GameObject Cube. Еслиоставить всем настройкам значения поумолчанию, вы получите препятствие,через котороеобъ- ектNavMeshAgent пройти несможет— онстолкнется с препятствием иостановится. Проверьтеполе Carve — оно заставляет препятствие создать в сеткеNavMesh подвижное отверстие, которое следует за объектом GameObject. Теперь объектGameObject подвижного препятствия может помешать игроку подняться или спуститься по наклонной плоскости. Так как высота NavMesh выбрана равной3, если препятствие имеет высотуменее3единиц, оно создаст отверстие внаходящейсяподнимсетке NavMesh. Есливысотапрепятствияпревышаетпорог,отверстие исчезает. В руководстве Unity приведено подробное — и доступное! — объяснение различных компонентов. Щелкни- те на кнопке Open Reference ( ) в верхней части панелиNavMeshObstacle в окне Inspector, чтобы открыть руководство. Не пожалейте времени и ознакомьтесь с ним — в нем отлично описаны различные варианты. Свойства Shape, Center и Size позво- ляют создать препятствие, ко- торое только частично блокирует NavMesh Agent. Если у вас имеется объект GameObject нестандарт- ной формы, вы можете добавить несколько компонентов NavMesh Obstacle, чтобы создать несколько разных отверстий в сетке NavMesh.
Head First C# Лабораторный курс Unity No 6 699 Лабораторный курс Unity No6 Добавление сценария для перемещения препятствия вверх и вниз Всценарии используетсяметодOnMouseDrag.Онработаеттак же, каки метод OnMouseDown изпоследней лабораторнойработы, еслине считать того, что он вызываетсяприперетаскиванииобъектаGameObject. public class MoveObstacle : MonoBehaviour { void OnMouseDrag() { transform.position += new Vector3(0, Input.GetAxis("Mouse Y"), 0); if (transform.position.y < 1) { transform.position = new Vector3(transform.position.x, 1, transform.position.z); } if (transform.position.y > 5) { transform.position = new Vector3(transform.position.x, 5, transform.position.z); } } } Перетащитесценарий на объектGameObject MovingObstacleи запуститеигру.Ой-ой,что-то нетак. Выможете щелкать и перетаскивать препятствие вверх-вниз, но при этом также перемещается игрок. Чтобы исправить ошибку, добавим тег кGameObject. Теперь изменим сценарийMoveToClick, чтобы проверить тег: if (Physics.Raycast(ray, out hit, 100)) { if (hit.collider.gameObject.tag != "Obstacle") { agent.SetDestination(hit.point); } } Снова запустите игру. Если щелкнуть на препятствии, вы смо- жете перетащитьего вверх и вниз, и оно остановится, когда достигнет пола или поднимется слишком высоко. Щелкните в любом другомместе,иигрокпойдет туда,какипрежде.Теперь вы можете поэкспериментировать с настройкамиNavMesh Obstacle(этобудетпроще сделать, если выуменьшите значение Speed внастройкахNavMeshAgentобъектаPlayer): ≥ Запустите игру. Щелкните в строкеMovingObstacle в окне Hierarchy и снимитефлажок Carve. Переместите игрока на верх наклонной плоскости, щелкните на нижнейчасти наклонной плоскости — игрок столкнется с препятствием и остановится. Перетащите препятствие вверх, и игрок снова начнетдвигаться. ≥ Теперь установитефлажокCarve и попробуйте сделать то же самое. Приперемещении препятствия вверх и вниз игрок пересчитывает свой маршрут, выбирая долгий путь для обхода препятствия, если оно опущено, и меняя курс вреальном времениприперемещении препятствия. Ранеедля работыс колесом мышииспользовалсяметод Input.GetAxis. Теперь перемещение мышивверх-вниз (по осиY) используетсядля перемещенияпрепятствия посредством изменения его позиции Y. Назначьте тег объекту препятствия так же, как это было сделано в послед- ней лабораторной работе, но на этот раз выберите команду «Add tag...» в раскрывающемся списке, а затем воспользуйтесь кнопкой для до- бавления нового тега Obstacle. Теперь вы можете воспользоваться раскры- вающимся списком для назначения тега объекту GameObject. В: Как работает сценарий MoveObstacle? Он использует += для обновления transform. position — означает ли это , что мы используем векторные вычисления? О:Да, и это отличная возможность лучше понять в ек торну ю арифм етику . Input.G etA x is в озв ращае т положительное число, если мышь двигается вверх, или отрицательное, если мышь двигается вниз (по- пробуйтедобавитькомандуDebug.Log, чтобыувидеть возвращаемое значение). Препятствиеначинается в позиции(–5 .75 ,1 , – 1).Если игрокперемещаетмышь вверх и методGetAxis возвращает0.372 , операция += прибавляет к позиции вектор(0, 0.372, 0). Это означает, что оба значения X суммируются для получения нового значения X, после чего то же самоеделается созначениямиY иZ.Таким образом, новая позицияYравна1 +0.372 =1.372, апоскольку к значениям X иZ прибавляются нули, изменяется толькозначениеY, чтоприводиткперемещениювверх. Первая команда if не позволяет прямоугольнику уходить ниже уровня пола, а вторая не позволяет ему подниматься слишком высоко. Сможете разобраться в том, как они работают? hit.collider содержит ссылку на объ ект , с которым с тол кнул ся лу ч. часто Задаваемые вопросы
700 глава 12 Лабораторный курс Unity No6 ¢ При предварительном построении NavMesh указыва- ется максимальный угол наклона и высота ступени, чтобы агенты NavMesh Agent могли перемещаться по наклонным плоскостям и лестницам. ¢ Также вы можете задать высоту агента, чтобы в сетке создавались отверстия вокруг препятствий, слишком низких для того, чтобы агент обходил их. ¢ При перемещении объекта GameObject по сцене NavMesh Agent избегает препятствий (и возможно, других агентовNavMesh Agent). ¢ На метке под манипулятором Scene изображен зна- чок, который показывает, что сцена находится в режи- ме перспективы (дальние объекты выглядят меньше, чем ближние) или в изометрическом режиме (все объ- екты сохраняют свои размеры независимо от дально- сти).Значок может использоватьсядля переключения междудвумя режимами. ¢ Методtransform.LookAt направляет объектGameObject в заданную позицию. С его помощью можно направить камеру на объект GameObject в сцене. ¢ Вызов Input.GetAxis("Mouse ScrollWheel") возвраща- ет число (обычно в диапазоне от –0.4 до 0.4), пред- ставляющее величину смещения колеса мыши (или 0, если колесо не двигалось). ¢ Вызов Input.GetAxis("Mouse Y") позволяет отслежи- вать перемещения мыши вверх/вниз. Его можно объ- единить с методом OnMouseDrag, чтобы организовать перемещение объектов GameObject мышью. ¢ Добавьте компонентNavMesh Obstacle, чтобы сделать препятствия, которые могут создавать подвижные от- верстия вNavMesh. ¢ Класс Input содержит методыдля получения ввода во время выполнения метода Update (например, Input. GetAxis для перемещения мыши и Input.GetKey для ввода с клавиатуры). Проявите фантазию! Сможетеливынайтивозможностидля того,чтобыулучшить игру(азаодно попрактиковатьсявпрограммировании)?Несколькоидей, которые помогут вам проявитьфантазию: ≥ Дополните сцену — добавьте в нее больше наклонных плоскостей, лестниц, платформ и препятствий.Найдите нестандартные возмож- ности для использования материалов. Поищите в интернете новые текстурные карты. Постарайтесь, чтобы сцена выглядела интересно! ≥ Ускорьтеперемещение NavMeshAgent,когда игрокудерживает нажа- той клавишуShift.Проведите в документацииScriptingReferenceпоиск по тексту «KeyCode», чтобы найти коды левой/правой клавишShift. ≥ В предыдущей лабораторной работе использовались методы OnMouseDown, Rotate, RotateAround и Destroy. Попробуйте исполь- зовать ихдлясозданияпрепятствий, которые вращаютсяили исчезают по щелчку. ≥ Реальной игры пока нет — игрок просто перемещается по сцене. Сможетеливыпредложить способ преобразовать вашу программу в полосу препятствий стаймером? Вы уже знаете о Unity вполне достаточно, чтобы начать строить интересные игры, — это отличныйспособ накопить практический опыт, чтобы статьболее эффективнымразработчиком. Вам предостав- ляется возмож- ность поэкспе- риментировать. Проявление творческой фан- тазии — очень эффективный способ повы- шения вашей квалификации программиста. КЛЮЧЕВЫЕ МОМЕНТЫ
дальше 4 701 Упражнение: поиск пар — сражение с боссом Есливы играли в видеоигры(а мы в этом твердо уверены!), наверняка вам при- шлось часто сражаться с боссами — такие сражения обычно происходят в конце уровняилиегочасти,а противник оказывается сильнее иопаснее тех, которыевстречались вам ранее. Передвамипоследнее упражнение, завершающее этукнигу, — считайте, что это своегорода «сражениесбоссом». В главе 1 мы построили игру с поиском пар животных. Приложение стало неплохим началом, но в нем... чего-то не хватает. А вы сможете придумать, как преобразоватьэто приложение вреаль- ную игру? Перейдите к репозиторию GitHub данной книги и загрузите PDF-файл проекта — или, если вы предпочитаете сражаться с боссами врежимеHard, попробуйте справиться с задачей самостоятельно. Книга подходит к концу, но обучение продолжается. Мы подготовили еще немало за- гружаемых материалов по важным темам C#. В частности, в продолжение вашего пути изучения Unity в них представлены дополнительные лабораторные работы Unity и даже новое «сражение с боссом» из области Unity. Надеемся, вы многое узнали из этой книги, но что еще важнее, мы верим, что ваш путь изучения C# только начинается. Хороший разработчик никогда не перестает учиться. За дополнительной информацией обратитесь крепозиторию GitHub данной книги: https://github.com/head-first-csharp/fourth-edition. еще одно упражнение
поздравляем — путешествие продолжается Спасибо за то, что вы прочитали нашу книгу! Похлопайте себя по плечу — это настоящее достижение! Мы надеемся,что это путешествиебыло для вас таким жеприятным, каки для нас, ичто вам понрави- лись всепроекты икод, которыйбыл приэтом написан. Постойте, это еще не все! Путешествие только началось... Внекоторых главах приводились ссылкина дополнительные проекты,которые можно загрузить с нашей страницы GitHub: https://gitgub.com/head-first-csharp/ fourth-edition На GitHub также доступенбольшой объем дополнительных материалов. Из них вы сможетеузнать много нового инайдете ряд новыхпроектов! Неостанавливайтесь напутиизученияC#!ВзагружаемыхдокументахPDF продолжится ваша историяизученияC#ирассматриваются некоторые важныетемыC#,средикоторых: ≥ обработчикисобытий; ≥ деле га ты; ≥ паттернMVM(включаяпроектаркаднойигры встиле«ретро»); ...и многоедругое! ИразужвызашлинаGitHub,обратитевниманиена дополнительные материалыоUnity.Вы сможетезагрузить: ≥ PDF-версиивсех лабораторныхработUnity из книги. ≥ НовыелабораторныеработыUnity, в которыхиспользуетсяфизика, коллизии и многое другое! ≥ Завершающее испытание, которое проверит все ваши навыки разработки Unity. ≥ Полныйпроект лабораторнойработыUnity,в котором с нуля создается компьютернаяигра. А также обратите внимание на пару важных (и отлично написанных!) книг от издательства , созданных нашими друзьями и коллегами. «Сборник рецептов для разработчика Unity» поможет вам поднять ваши навыки работы с Unity на следующий уровень. Множество ис- ключительно полезных приемов и инструмен- тов представлено в ней в форме «рецептов», которые вы можете с ходу применить в своих проектах. «C# 8.0 in a Nutshell» должен прочитать каждый разработчик C#. Если вы хотите действительно глубоко изучить тот или иной аспект C#, вряд ли что-нибудь помо- жет вам лучше, чем эта книга . Обратите внимание на следую- щие замечательные ресурсы C# и .NET! Вступайте в сообщество раз- работчиков .NE T: https:// dotnet.microsoft.com/platform/ co mmun it y. Смотрите живые трансляции и общайтесь в чатах с группой разработчиков, занимающихся созданием .NET и C#: https:// dotnet.microsoft.com/platform/ community/standup. Обращайтесь к документации за новыми знаниями: https://docs. microsoft.com/en-us/dotnet
Приложение 1. Проекты ASP.NET Core Blazor Мы очень серьезно относимся к яблокам. Visual Studio для пользователей Mac Ваш Mac — полноправный гражданин мира C# и .NET. Мы писали эту книгу, ни на минуту не забывая о пользователях Mac; именно поэтому в книгу было включено это приложение. Большинство проектов в книге относится к категории консольных приложений, работающих как в Windows, так и на Mac. В некоторых главах приводятся проекты, постро- енные на базе технологий настольных приложений Windows. В этом приложении приводятся альтернативные версии этих проектов, включая полную замену главы 1, в которых C# используется для создания приложений Blazor Webassembly, выполняемых в браузере и эк- вивалентных Windows-приложениям. Все это делается вVisual Studio for Mac — превосход- ном инструменте для написания кода и ценного учебного инструмента для изучения C#. А теперь займемся программированием!
704 Visual Studio для пользователей Mac почему стоит изучать C# Зачем вам изучать C# C# — простой современный язык, на котором можнорешать совершенно не- вероятные задачи. ИзучаяC#,вы не ограничиваетесь только новым языком. C# открываетпередвамицелыймир .NET — невероятно мощнойплатформы с открытым кодом для построения самыхразнообразныхприложений. Visual Studio станет вашим окном в C# ЕсливыещенеустановилиVisualStudio2019,самое время это сделать. Откройте страницуhttps://visualstudio.microsoft.comи загрузитеVisualStudiofor Mac.(Еслионаужеустановленанавашемкомпьютере,запуститепрограммууста- новкиVisualStudioдляMac,чтобыобновитьнаборустановленныхкомпонентов.) Установка .NET Core После того как программа установки Visual Studio for Mac будет загружена, запуститеее,чтобыустановитьVisualStudio.Проследитеза тем, чтобыбыла выбрана целевая платформа .NETCore. Веб-приложения Blazor также можно строить в Visual Studio for Windows Вбольшинствепроектовэтойкниги создаются консольныеприложения .NET Core,которыеработают как в системе Windows, так и в macOS.В некоторых главах рассматриваются проекты, построенные на базе технологии WPF (Windows Presentation Foundation). Так как технология WPF работает только в Windows, мы написали это приложение, чтобы вы могли создавать эквивалентные проекты на Mac на базевеб-технологий,а именно проектыASP.NETCore BlazorWebAssembly. АесливыработаетевWindows,но хотитенаучитьсястроить веб-приложения срасширеннойфункцио- нальностью на базеBlazor? Тогда вам повезло! Проекты из этого приложения можно строить и вVisual Studio for Windows. Перейдите в программу установкиVisualStudio иубедитесь в том, что флажок ASP. NET and web development установлен. Ваши снимки экрана IDE небудут полностью совпадать с при- веденными в руководстве, но весь код останется неизменным. Убедитесь в том, что вы устанавлива- ете Visual Studio for Mac, а не Visual Studio Code. Visual Studio Code — прекрасный кросс­ платформенный редактор с откры- тым кодом, но он не так хорошо подходит для разработки .NET, как Visual Studio. Вот почему мы будем использовать Visual Studio в книге как учебный и аналитиче- ский инструмент. В проектах веб-приложений выбудете использовать HTML иCSS, но знания HTML или CSS от вас непотребуется. Этакниганаписана для тех, ктохочет изучить C#.В проектахиз этогоруководства вы будете создавать веб-приложенияBlazor, включающиестраницы,оформленные сприменениемHTMLиCSS.Небеспокой- тесь,если выеще неработали сHTMLилиCSS, — работа скнигой нетребует никакихпредварительных знаний в области веб-дизайна. Мы предоставим все необходимое для создания всех страниц проектов веб-приложений.Впрочем, попутно вы можете кое-чтоузнать оHTML. РЕЛАКС
дальше 4 705 начинаем программировать на C# Код C# можно писать и в Блокноте или в другом текстовом редакторе, но су- ществуетболееудобныйвариант. IDE(сокращение от Integrated Development Environment, т. е. «интегрированная среда разработки») включает в себя текстовый редактор, визуальный конструктор, менеджер файлов, отладчик... словномногофункциональныйинструмент длявсехопераций, которые могут понадобиться принаписаниикода. Перечислимлишь некоторые иззадач,в решениикоторыхвампоможетVisual Studio: БЫСТРО создавать приложения. ЯзыкC# гибок и прост в изучении, а VisualStudio позволяетавтоматизировать многиеоперации,кото- рые обычно приходится выполнять вручную. Примеры операций, которыеVisualStudio делаетза вас: ‘ Управлениефайлами проекта. ‘ Удобноередактирование кода проекта. ‘ Управление графикой, аудио, значками и другими ресурсами проекта. ‘ Отладка кода с возможностью выполнения впошаговомрежиме. Написаниеивыполнениекода C#. VisualStudioIDE— одиниз самых простыхиудобныхинструментовдлянаписаниякода. Группаразработки изMicrosoft приложила колоссальныеусилияк тому, чтобы написание кода было по возможности простым иудобным. Построение восхитительных в визуальном отношении программ. В этомприложении вы будете строить веб-приложения, работающие вбраузере.Мыбудемиспользовать Blazor —технологию, которая позво- ляетстроить интерактивныевеб-приложениянаC#. При объединении C# с HTML иCSS в вашемраспоряжении появляется впечатляющий инструментарий веб-разработки. Изучение и исследование C# и .NET. Visual Studio представляет со- бойинструментразработкимировогоуровня, но ксчастью длянас, это также великолепныйучебныйинструмент. МыбудемиспользоватьIDEдля исследованияC#,что позволитвам быстро закрепить важныеконцепции программирования ввашем мозге. 1 2 3 4 Visual Studio — инструмент для написания кода и изучения C# Visual Studio — замечательная среда разработки, но мы также бу- дем использовать ее как учебный инструмент для изучения C#. В книге Visual Studio будет называться просто «IDE».
706 Visual Studio для пользователей Mac за дело Создание вашего первого проекта в Visual Studio for Mac Лучший способ изучения C# — непосредственное написание кода, поэтому мы воспользуемсяVisual Studio для создания нового проекта... и немедленно начнем писать код! Создайте новый проект Console Project. ЗапуститеVisual Studio2019for Mac.Припервом запускена экране появитсяокнодлясозданияновогопроекта илиоткрытияужесуще- ствующего. Щелкните на ссылкеNew, чтобысоздать новыйпроект. Есливызакроете окно, не беспокойтесь:чтобывызвать егообратно, достаточно выбрать вменю команду File>>New Solution...(⇧⌘N). Выберите на панели слева платформу .NET, а затем тип проекта Console Project. 1 Когда вы видите в тексте врезку Сделайте это! (илиПринципы отладки), откройте Visual Studio и вы- полните приведен- ные инструкции. Мы точ но расск ажем , что следуетделать и на что нужно обратить внимание, чтобы извлечь максимум пользы из приведен- ного примера. Выберите тип Console Project и убедитесь в том, что выбран язык C#. Выберите .NET. Щелкните на ссылке New, чтобы создать новый проект. Так создаются проекты ConsoleApp в Visual Studio 2019for Mac. Иногда мы будем называть его проектом консольного приложения .NET Core или консольного приложения(.NET Core). Существуют и другие разно- видности консольных приложений, но на разных плат- формахработают только консольные приложения .N ET Core, поэтому в Visual Studio for Mac мы будем создавать именно этуразновидность. Затем щелкните на кнопке Next. Сделайте это!
дальше 4 707 начинаем программировать на C# Присвойте новому проекту имя MyFirstConsoleApp. Введите MyFirstConsoleAppв полеProjectName инажмитекнопкуCreate, чтобысоздать проект. Просмотрите код нового приложения. Присозданиинового проектаVisual Studio даетвам отправную точкудлядальнейшейработы. Кактолькосоздание новых файлов для приложения будет завершено, на экране должен от- крытьсяфайл с именемProgram.csсо следующим кодом: 2 3 При создании нового проекта консольного приложения Visual Studio автоматически добавляет класс с именем MainClass. Класс изначально содер- жит метод с именем Main с одной командой для вывода строки текста на консоль. Классы и методы намного подробнее рассматривают- ся в главе 2. Проект можно разместить в любой папке, но IDE по умолчанию сохраняет его в папке Projects в вашем домашнем каталоге. В Windows используется другое имя главного класса. Когда вы создаете консольное приложение в Visual Studio for Windows, сгенерированный код почти не отличается от кода, генерируемого в Visual Studio for Mac. Одно из отличий заключается в том, что на Mac главный класс называется MainClass, а в Windows ему присваивается имя Program. В большинстве про- ектов книги это ни на что не влияет (исключения будут особо отмечаться в тексте). Будьте осторожны!
708 Visual Studio для пользователей Mac использование ide для запуска приложений Запуск приложений в Visual Studio IDE Рассмотрите среду Visual Studio IDE — и файлы, которые она создала за вас. Присоздании нового проекта Visual Studio автоматически создает несколько файлов за вас иобъединяетих в решение(solution).В окнеSolution влевой частиIDEпоказаны этифайлы, а решение (MyFirstConsoleApp) находится на верхнем уровне иерархии. Решение содержит проект,имякоторого совпадаетс именем решения. 1 Главноеокно позволяет редактировать код C#. Вы можете открыть несколько файлов с кодом в разных вкладках. Это окно Solution. В нем выводится спи- сок файлов решения MyFirstConsoleApp, ко- торый содержит проект (ему также присвоено имя MyFirstConsoleApp). Щелкните правой кнопкой мыши нафайле Program.cs, чтобы от- крыть его в Finder. Папка проекта приложения, созданная Visual Studio. В ней находятся все файлы решения, а также папки "bin" и "obj", генерируемые IDE при запуске приложения.
дальше 4 709 начинаем программировать на C# Запустите новое приложение. Приложение, которое среда Visual Studiofor Mac создала за вас, готово к запуску. Найдите вверхней частиVisualStudioIDEкнопкуRun(кнопка с треугольником)и нажмитеее: Просмотрите результаты выполнения программы. Когда вызапускаетевашупрограмму,в нижней частиIDE появляется окнотерминала срезуль- татом выполнения вашей программы: Лучшийспособ изучить язык — писать на нем побольше кода, поэтому в этойкниге мыбудем строить множество программ. Многие из нихбудут проектами консольных приложений, по- этомудавайте повнимательнее посмотрим,что жебыло сделано. Вверхнейчастиокна терминала выводятсярезультаты выполнения программы: Hello World! Щелкнитевлюбой точке кода, чтобы скрыть окно терминала. Затем нажмите кнопку внижней частиIDE,чтобыоткрыть егоснова, — выувидите тот же вывод вашей программы.IDEавтоматическискрывает окнотерминала послевыхода изприложения. НажмитекнопкуRun, чтобыснова запустить вашупрограмму.Выберите командуStartDebugging изменю Run иливоспользуйтесь комбинациейклавиш(⌘↩).Такбудут запускатьсявсе проекты консольныхприложенийв книге. 2 3 Результат выпол- нения консольного приложения выводит- ся в окне терминала в нижней части IDE. Если щелкнуть в любой другой точке IDE, окно терминала ис- чезнет, но его можно вернуть при помощи кнопки Terminal в нижней части окна. Кнопка закрепляет окно терминала на экране. В окнетерминала выводятся результаты консольных приложений, но этим его функциональность не ограничивается. Щелкните на кнопке <> в правой части окна терминала или выберите коман- дуView>>Terminal из меню, когда приложение не выполняется. Вы увидите оболочку терминала macOS внутри IDE, которая может использоваться для выполнения командной оболочки macOS: Щелкнитенакнопке еще несколько раз — IDE откроет несколько окон терминала одновремен- но. Вы можете переключаться между ними командой меню View>>OtherWindows или кнопкой на панели в нижней частиIDE: Подсказки для IDE: Открытие терминала из IDE Окнотерминала будетиспользо- ваться для запуска консольных при- ложений, которые будут строиться в книге, и взаимодействия с ними.
710 Visual Studio для пользователей Mac основы проектирования игр Давайте построим игру! Вытолькочтопостроилисвое первоеприложениеC#,и это замечательно! Атеперь попробуем создать что-то посложнее . Мы построим игру, в ко- торой игрок должен подбирать пары животных. На экраневыводится квадратная сетка с16 животными, а игрокщелкаетна парах, чтобы они исчезали с экрана. Игра с подбором пар животных, которую мы построим. Игра является приложением Blazor WebAssembly Консольныеприложения прекрасно подходят для ввода и вывода текста. Есливыхотитепостроить визуальноеприложение,котороеотображает- ся как страница в браузере, придется использовать другую технологию. Вот почему игра с подбором пар животных будет приложением Blazor WebAssembly. Blazor позволяет создавать полнофункциональные веб- приложения, которыемогутработать влюбом современномбраузере. Во многих главах книги будет представлено одно приложениеBlazor. В этом разделе мы кратко представим Blazor и опишем средства для построения какполнофункциональныхвеб-приложений, таки консольныхприложений. На экране выводятся восемь пар разных животных, случайным обра- зом распределенных в сетке. Игрок щелкает на двух животных: если эти животные одинаковы, то пара исчезает со страницы. Таймер следит за тем, сколько времени потребова- лось игроку для завершения игры. Цель игрока — най- ти все пары за минимальное количество времени. К тому времени, когда вы завер- шите этот проект, вы гораздо лучше освоите те инстру- менты, которые будут использо- ваны в книге для изучения и иссле- дования возмож- ностей C#. Построение проектов разных типов является важной частью изучения C#. Мы выбрали Blazor для проектов Mac в этой книге, потому что эта платформа предоставля- ет средствадля построе- ния полнофункциональ- ных веб-приложений, работающих в любом современном браузере. Но язык С# не огра- ничивается веб- программированием и консольными приложе- ниями! У каждого проекта в этом руководстве су- ществует эквивалентный проект Windows. Вы работаете в Windows, но хотите изучить Blazor и строить веб- приложения на C#? Что же, вам повезло! Все проекты, представлен- ные в этом руководстве, также могут быть постро- ены в Visual Studio for Wi nd ows.
дальше 4 711 начинаем программировать на C# Как построить игру Воставшейсячасти этогоразделабудет рассмотренпроцесспостроенияигры с поиском пар животных, которыйсостоит изнесколькихшагов: 1. Сначала мысоздадим проектBlazorWebAssembly AppвVisualStudio. 2. Затем мы создадим макет страницы и напишем код C# для случайной расстановкиживотных вокне. 3. Игрокщелкаетна парныхэмодзи,чтобыудалить ихизокна. 4. Мынапишем код C#,проверяющий условиепобеды. 5. Наконец,чтобыиграсталаболее интересной,мыдобавимв нее таймер. Обращайте внимание на врезки «Разработка игр... и не только», встре- чающиеся в тексте книги. Мы будем использовать принципы проектиро- вания для изучения и исследования важных концепций и идей программиро- вания, применимых в любых проектах, не только в видеоиграх. Создание проекта Случайная раССтановка животных обработка щелчков проверка победы игрока добавление та йм ера Что такое игра? Вроде бы ответ на этот вопрос совершенно очевиден. Но задумайтесь на минутку — все не так просто, как кажется на первый взгляд. • У всех ли игр есть победитель? У каждой ли игры есть конец? Не обязательно. Как насчет имитатора полета? Игры, в которой вы строите парк развлечений? Или таких игр, как The Sims? • Игры всегда интересны? Не для всех. Некоторым игрокам нравится процесс «гринда», когда им при- ходится делать одно и то же раз за разом; другим это кажется ужасным. • Всегда ли игры сопряжены с принятием решений, конфликтами или решением задач? Нет, не всегда. «Симуляторы ходьбы» — класс игр, в которых игрок просто исследует игровую среду, и часто в них вообще нет никаких головоломок или конфликтов. • Обычно довольно трудно четко определить, что же именно следует считать игрой. В учебниках по раз- работке игр можно найти множество разных определений. Для нашихцелей определим смысл «игры» (покрайней мере для хороших игр) следующим образом: Игра — программа, взаимодействовать с которой не менее увлекательно, чем строить ее. Проект может за- нять от 15 минут до часа в зависимо- стиоттого,ска- кой скоростью вы вводите текст. Нам лучше думается, ког - да мы не торопимся, так что выделите побольше времени. Разработка игр... и не только
712 Visual Studio для пользователей Mac создание нового веб-приложения Создание проекта Blazor WebAssembly в Visual Studio Построение новой игры начинается с созданиянового проекта вVisualStudio. ВыберитеFile>>NewSolution...(⇧⌘N), чтобывызватьна экран окноNewProject. Этоокноужевстречалось вам присоздании проекта консольного приложения. Выберите категорию App в разделе «Web and Console» слева, затем выберите вариантBlazorWebAssemblyAppи щелкнитена кнопке Next. IDEоткрывает страницу с параметрами конфигурации. Оставьтевсемпараметрам значения поумолчанию ищелкнитена кнопке Next. 1 2 Сначала выбе- рите App... ... зат ем выберите Blazor WebAssembly App... ...и щелкните на кнопке Next.
дальше 4 713 начинаем программировать на C# Введитеимя проектаBlazorMatchGame,какэто делалось впроектеконсольного приложения. Затем щелкните на кнопкеCreate, чтобы создать решение. IDEсоздает новый проектBlazorMatchGame и выводит его содержимое, как и в консольном приложении. Раскройте папкуPages вокнеSolution,чтобы просмотреть еесодержимое,а за- тем сделайте двойной щелчок на файлеIndex.razor,чтобыоткрыть его вредакторе. 3 4 Если у вас возникнут какие-либо проблемы с этим проектом, зайдите на нашу страницу GitHub и найдите ссылку на видео- ролик: https://github.com/head-first-csharp/fourth-edition
714 Visual Studio для пользователей Mac ваше приложение выполняется в браузере Запуск веб-приложения Blazor в браузере Запущенное веб-приложение состоит из двухчастей: сервера и веб-приложения. VisualStudioзапускает их по нажатию однойкнопки. Выберите браузер для запуска веб-приложения. Найдитекнопку Run(кнопка с треугольником)вверхнейчастиVisualStudioIDE. Браузер поумолчанию отображаетсярядом с подсказкой Debug >. Щелчокна именибраузера открываетраскрывающийся списокустановленныхбраузеров— выберитеMicrosoftEdgeили Google Chrome. Запустите веб-приложение. Щелкнитена кнопкеRun, чтобы запустить приложение. Также можно выбрать команду Start Debugging(⌘↩)изменюRun.IDEсначала открываетокноBuildOutput(внижнейчасти, как с окном терминала), а затем окно Application Output. После этого открывается браузер,в ко- тором выполняетсявашеприложение. 1 2 Запускайте веб-приложения в Microsoft Edge и Google Chrome. В Safari веб-приложения прекрасно работают, но вы не сможете их отла - живать. Отладка веб-приложений поддерживается только в Microsoft Edge и Google Chrome. Edge можно загрузить по адресу https://microsoft.com.edge, а Chrome — по адресу https://google.com/chrome; оба браузера бесплатные. Сд ел айте э то! Будьте осторожны!
дальше 4 715 начинаем программировать на C# Сравните код в Index.razor с тем, что вы видите в браузере. Веб-приложение вбраузере состоитиздвухчастей: навигационноеменюв левойчастисодержит ссылкина различныестраницы(Home, CounterиFetchData), асправаотображаетсястраница. СравнитеразметкуHTML в файлеIndex.razor с приложением, отображаемым вбраузере. 3 Замените «Hello, world!» другим текстом. Изменитетретью строкуфайла Index.razor, чтобы она содержала другойтекст,напри- мер такой: <h1>Elementary, my dear Watson.</h1> Теперь вернитесь вбраузериперезагрузитестраницу.Подождите немного... нет, ничего не изменилось —все равно выводитсятекст «Hello, world!». Деловтом, чтовыизменили код,но не обновили сервер. Щелкнитена кнопке Stop иливыберите командуStop(⇧⌘↩)изменюRun. Теперь вернитесь иперезагрузитестраницу вбраузере;таккакприложениебыло остановлено, выводится страница с сообщением о недоступности сайта. Снова запустите приложение и перезагрузитестраницу вбраузере. На этотраз вы увиди те о бно вленны й текс т. 4 Нет ли у вас лишних экземпляров открытых браузеров? Visual Studio открывает новый браузер каждый раз, когда вы запускаете веб-приложение Blazor. Возьмите в привычку закрывать браузер (⌘Q), прежде чем останавливать приложение (⇧⌘↩). Попробуйте скопи- ровать URL-адрес из браузера, от - кройте новое окно Safari и вставьте его. Ваше при- ложение будет работать и здесь. Два разных браузе- ра подключаются к одному серверу.
716 Visual Studio для пользователей Mac начинаем построение игры Создание проекта Случайная раССтановка животных ОбрабОтка щелчкОв Проверка Победы игрока Добав- ление тай мер а вы находитеСь здеСь Теперь можно переходить к написанию кода игры Новоеприложениесоздано,а среда Visual Studio сгенерировала необходимые файлы. Теперь добавим кодC#,который обеспечитработу игры(а такжеразметкуHTML,определяющую ееоформление). Когда вы вводите код C#, он должен быть абсолютно правильным. Некоторые считают, что вы становитесь настоящим разработчиком только после того, как впервые проведете несколько часов в поисках не- правильно поставленной точки. Регистр символов важен: SetUpGame и setUpGame — разные имена. Лишние запятые, круглые скобки, точки с запятой и т. д. могут нарушить работо- способность вашего кода или, что еще хуже, изменить ваш код, чтобы он успешно строил- ся, но работал совершенно не так, как предполагалось. Функция IDE IntelliSense помогает избежать подобных проблем... но и она не сможет сделать все за вас. Мы переходим к работе с кодом C#, который находится в файле Index.razor. Файл срас- ширением .razor содержит страницуразметки Razor. Razor объединяетразметку страницы HTML с кодом C#, все это в одном файле. Мы добавим в этот файл код C#, который опреде- ляет поведение игры, включая код добавле- ния эмозди на страницу, обработки щелчков мышью и управления таймером. Когда вы создавали свое консоль- ное приложение ранее в этой главе, код C# содержался в файле с именем Program.cs — когда вы видите расширение .cs, оно указывает на то, что файл со- держит код C#. Будьте осторожны!
дальше 4 717 начинаем программировать на C# Как работает разметка страницы в игре Вигре с поискомпариспользуетсясетка— покрайнеймере на первыйвзгляд.На самом делемакет состоит из16квадратныхкнопок. Есливыизменитеразмерыокнабраузера исделаетеего очень узким, кнопкивыстроятся водиндлинныйстолбец. Чтобы определить макет страницы,мы сначала создаем контейнер шириной400пик- селов («пиксел» CSS составляет1/96 дюйма, когда браузер использует масштаб по умолчанию), который содержит кнопки шириной 100 пикселов. Мы приведем весь код C# и HTML, которыйнеобходимо ввести в IDE.Внимательно следите за кодом, которыйвскоребудет добавленв проект,здесь происходитвсе«волшебство» с объеди- нением кода C# с HTML: <div class="container"> <div class="row"> @foreach (var animal in animalEmoji) { <div class="col-3 "> <button type="button" class="btn btn-outline-dark"> <h1>@animal</h1> </button> </div> } </div> </div> Символ @ включает код C# в страницу Razor. Это цикл foreach, который многократно выполняет один и тот же код, чтобы сгенерировать кнопку длякаждого эмодзи в списке. Цикл foreach повторяет весь код, заключенный в фигурные скобки { }, по одному разудля каждого эмодзи в списке, заменяя @animal каждым эмодзи в списке. Так как список состоит из16 эмодзи, в результатебудет сгенерирована серия из 16 кнопок.
718 Visual Studio для пользователей Mac добавление эмодзи в приложение Visual Studio помогает в написании кода C# Blazor позволяетсоздавать интерактивные,полнофункциональныеприложения,сочетающие разметкуHTMLс кодомC#.Ксчастью, VisualStudioIDEсодержит полезнуюфункциональность, которая помогаетписать код C#. Добавьте код C# в файл Index.razor. Начнитес добавления блока@code в конец файла Index.razor . (Текущее содержимоефайла пока нетрогайте— онобудет удаленопозднее.)Перейдитекпоследнейстрокефайла ивведите @code{.IDEвставляетзакрывающуюфигурную скобку}завас.НажмитеклавишуEnter,чтобы добавить строку между двумя скобками: Пользуйтесь окном IntelliSense в IDE, упрощающим написание кода C#. Установите курсор в строке между {фигурными скобками} и введите букву L. IDE открывает окно IntelliSense с вариантамиавтозавершения. Выберите вокне вариант List<>: IDEподставляет List в код. Добавьте открывающуюугловую скобку (знак «меньше») <;IDE автоматическидобавляетзакрывающую скобку >и устанавливаеткурсор междускобками. Начните создавать список List для хранения эмодзи. Введите s, чтобы открыть следующее окно IntelliSense: Выберитевариант string—IDEдобавляет его междуугловымискобками. Нажмитеклавишусо стрелкой¦, затем пробел ивведитеanimalEmoji=new.Снованажмите клавишупробел, чтобы открыть следующееокно IntelliSense.НажмитеEnter,чтобы выбратьзначениепоумолчанию, List<string>, из списка вариантов. Послеэтого код должен выглядеть так: List<string> animalEmoji = new List<string> 1 2 3 Окно IntelliSense в IDE помогает писать код C#, предлагая полезные варианты автозавершения. Варианты перебира- ются клавишами со стрелками, а выбор осуществляется клавишей Enter (также можно воспользоваться мышью).
дальше 4 719 начинаем программировать на C# Завершите создание списка эмодзи. Начнитес добавления блока@code вконец файлаIndex.razor . Перейдите кпоследней строке ивведите@code{.IDEвставитзакрывающуюфигурную скобку}завас. НажмитеEnter, чтобы вставить строку между скобками,затем: ‘ Введите открывающую круглую скобку( —IDE автоматически вставляет парную за- крывающую скобку. ‘ Нажмите клавишу ¦, чтобы выйти из круглых скобок. ‘ Введите открывающуюфигурнуюскобку{—IDEтакже вставляет закрывающую скобку. ‘ НажмитеEnter, чтобы добавить строкумеждуфигурнымискобками, после чеговставьте точку сзапятой; после закрывающейфигурной скобки. Витогепоследниешесть строкв концефайлаIndex.razorдолжны выглядеть так: Поздравляем — вы только что создали свою первую команду C#. Но все только начинается! Высоздали список дляхранения эмодзи. Введите двойную кавычку " в пустойстроке — IDE добавит закрывающую кавычку. Используйте Character Viewer для ввода эмодзи. Затем выберите команду Edit >> Emoji &Symbols (^⌘Space), чтобы запустить программу macOS Character Viewer. Установите курсор между кавычками, а затем перейдите в Character Viewer ипроведите поиск по строке «dog»: Последниешесть строкв конце файлаIndex.razorпринимаютследующийвид: 4 5 Сделайте двойной щелчок на эмодзи с собакой, чтобы вставить его в код между кавычками, как если бы вы ввели его с клавиатуры. О том, как работают спи- ски List, рассказано в главе 8. За дополнительной инфор- мацией о командах обра- щайтесь к главе 2.
720 Visual Studio для пользователей Mac эмодзи во множественном числе Завершение создания списка эмодзи и вывод их в приложении Вы только что добавили эмодзи с изображением собаки в список animalEmoji. Теперь добавим второй эмодзис собакой — поставьте запятую послевторойкавычки, затем пробел, еще одну кавычку, эмодзи, кавычкуи запятую: Добавьтепослеэтой строки другую,котораявыглядитточно также,но вместо эмодзи с собакой в ней используютсяэмодзи сволком. Добавьтееще шесть аналогичныхстрокс парами коров, лис, кошек, львов, тигрови хомяков.Врезультатесписок animalEmojiдолжен содержать восемь пар эмодзи: Замена содержимого страницы Удалите следующиестроки вначалестраницы: <h1>Elementary, my dear Watson.</h1> Welcome to your new app. <SurveyPrompt Title="How is Blazor working for you?" /> Затемустановитекурсор в третьей строкеивведите <st — IDEоткрываетокноIntelliSense: Выберите всписке пункт style, затем введите>.IDEдобавляет закрывающийтегHTML: <style></style> IDEпомогает создавать разметкуHTMLстраницы — в данном случае создается тег HTML. Если вы не знаете HTML, это нормально; в книге будет приведен весь код, необходимый дляработы приложений. IDE автоматически расставляет отступы во всем вводимом кодеC#. Но когда вы вводите эмодзи или теги HTML, может оказаться , что автоматические отступы не всегда выглядят так, как вы хотите. Проблема легко решается: выделите текст и нажмите ⇥ (Tab), чтобы создать отступ, или ⇧⇥ (Shift+Tab), чтобы удалить отступ. Подсказки для IDE: отступы
дальше 4 721 начинаем программировать на C# Установите курсор между <style> и </style>, нажмите Enter; аккуратно введите следующий код. Убедитесь в том,что кодвашего приложения точно соответствуетприведенному. <s ty le> .c ontainer { width: 400px; } button { width: 100px; height: 100px; font-size: 50px; } </style> Перейдитекследующейстрокеивоспользуйтесь окномIntelliSenseдля ввода открывающегои закры- вающего тега <div>,какбыло сделано ранее для <style>. Затем тщательно введите следующий код (проследитеза тем,чтобы он точно совпадал с приведенным втексте): <div class="container"> <div class="row"> @foreach (var animal in animalEmoji) { <div class="col-3"> <button type="button" class="btn btn-outline-dark"> <h 1> @ani mal </h 1> </button> </div> } </div> </div> Убедитесь в том, что экран приложения похож на этот снимок экрана. Тогда вы будете точно знать, что код введен без опечаток. Если ранее вы ужеработали cHTML, вы замети - те конструкции @foreach и @animal, непохожие на стандартные элементы HTML. Это код Blazor — код C#, встраиваемый непосредственно в HTML. Экран игры строится из кнопок. Она реализуется очень простой таблицей стилей CSS, которые за- дают общую ширину контейнера, а также высоту и ширину каждой кнопки. Так как ширина контей- нера составляет 400 пикселов, а ширина каждой кнопки — 100 пикселов, страница допускает раз- мещение только четырех столбцов в строке, после чего добавляется разрыв строки; таким образом образуется некое подобие сетки. Каждая кнопка на странице со- держит изображение животного. Игрок нажимает кнопки, чтобы выбрать совпадающие пары.
722 Visual Studio для пользователей Mac добавление кода случайной перестановки эмодзи Перестановка животных в случайном порядке Еслибывсепары животныхрасполагались пососедству, играбылабыслишком простой.Добавим код C#дляслучайнойперестановкиживотных,чтобыонирасполагались вслучайномпорядке каждыйраз, когда игрок перезагружаетстраницу. Установитекурсор после точки с запятой; над закрывающейфигурной скобкой}вконце файлаIndex.razor идважды нажмите Enter. Затем воспользуйтесь окномIntelliSense, какэто делалось ранее, для ввода следующей строки кода: List<string> shuffledAnimals = new List<string>(); Затемвведитеprotectedoverride(IntelliSense может автоматически завершить эти ключевые слова).Кактолько вывведетеэтиключевыесловаипробел,откроетсяокноIntelliSense— вы - берите OnInitialized() из списка: IDE автоматически заполняет код метода c именем OnInitialized (методы более подробно рассматриваются в главе 2): Замените вызов base.OnInitialized() вызовом SetUpGame(), чтобы вашметод выглядел так: protected override void OnInitialized() { SetUpGame(); } Затем добавьте следующий метод SetUpGame под методом OnInitialized— иснова окно IntelliSenseпоможет вам вэтом: private void SetUpGame() { Random random = new Random(); shuffledAnimals = animalEmoji . OrderBy(item => random.Next()) . ToList(); } Впроцессе вводаметодаSetUpGameможно заметить, чтоIDEчастооткрываетокнаIntelliSense, чтобыупростить вводкода. Чембольше вы пользуетесьVisualStudio длянаписания кода C#, темполезнее будут этиокна:современемвыубедитесь, что онизначительно ускоряютработу. Апокаиспользуйтеих дляпредотвращения опечатокпривводе— вашкоддолженполностью совпадать с приведенным в книге,иначеприложение небудетработать. 1 2 3 Вскоре выузна- етео методахC# намногобольше. Вы только что добавили методвсвоеприложение. Впрочем, если вы еще не на сто процентов пони- маете, что такое метод, это вполне нормально. Вследующейглавеметоды и структура кода C# рас- сматриваются намного подробнее. За дополнительной ин- формацией о методах обращайтесь к главе 2. РЕЛАКС
дальше 4 723 начинаем программировать на C# Прокрутите окно к разметке HTML и найдите следующий код: @foreach (var animal in anim alE moj i) Сделайте двойной щелчок на имени animalEmoji, чтобы выделить его, и введите символ s. IDEоткрывает окно IntelliSense. Выберите в списке пункт shuffledAnimals: Снова запустите приложение. Животные в списке переставляются в случайном порядке. Перезагрузите страницу вбраузере— порядокразмещения эмодзи изменяется. Каждыйраз, когда вы перезагружаетестраницу,эмодзибудутвыводитьсяв новом порядке. 4 Еще раз: убедитесь в том, что экран запущенного приложения похож на этот снимок экрана. Тогда вы будете точно знать, что код введен без опечаток. Двигайтесь дальше только после того, как ваша игра будет переставлять животных в случайном порядке при каждой перезагрузке страницы браузера.
724 Visual Studio для пользователей Mac построенное приложениеработает! отличная работа Игра работает в отладчике Когда выщелкаетена кнопкеRun( )или выбираете командуStartDebugging(⌘↩)из меню Run,чтобы запустить программу,VisualStudio переходитв режим отладки. Чтобыпонять, что приложениеработаетврежиме отладки, достаточно взглянуть на элементы отладки на панели инструментов. Кнопка Startзаменяется кнопкойStop <>, раскрывающийся списокдлявыборабраузераблокируется, ипоявляются новые элементыуправления. Наведитеуказатель мыши на кнопкуPauseExecution,чтобыувидеть подсказку: Чтобы остановить приложение, щелкните на кнопке Stop или выберите команду Stop (⇧⌘↩) из меню Run. Ого, игра уже неплохо выглядит! Все готово для следующего этапа работы. Построениеновойигры несводится кнаписанию кода — вытакжеработаетенадпроектом. Эффективныйспособ реализации проектов основан на построении их с не- большимиприращениями и тщательным анализом про- межуточных результатов, гарантирующим, что работа идет вправильномнаправлении. Притакомподходе увас будет достаточновозможностей, чтобы сменитькурспри нео бхо дим ос ти.
дальше 4 725 начинаем программировать на C# Поздравляем — высоздалиработающую программу!Разумеется,программированиенесколько сложнеепростого копированиякода изкниги. Но дажеесливы никогда неписаликодпрежде,вас удивит, сколько всего вы уже понимаете. Соедините линией каждую команду C# в левом столбце с описанием того, что делает эта команда, в правом столбце. Мы привели решение для первой команды,чтобы вамбыло проще. Команда C# Создает копииэмодзи,переставляетих вслучайном порядке исохраняетв списке shuffledAnimals. Что делает Создаетновыйгенератор случайныхчисел. Создаетсписок из восьмипар эмодзи. Настраиваетигрупри каждой перезагрузкестраницы. Конец метода, выполняющего настройкуигры. Создаетвторой списокдля хра- ненияпереставленныхэмодзи. Начало метода, выполняющего настройкуигры. Еще одно упражнение, в котором вам придется порабо- тать карандашом. Не жалейте времени на выполнение всех этих упражнений, потому что они помогут быстрее закрепить важные концепции C# в вашем мозге. Кто и что делает? ае ааеееееееееееееееее
726 Visual Studio для пользователей Mac не пропускайте упражнения Команда C# Что делает Следующее упражнение поможет вам лучше понять код C#. 1. Возьмите лист бумаги и поверните его набок, чтобы он лежал в альбомной ориентации. На- рисуйте вертикальную линию в середине. 2. Запишите весь код C# в левой части, оставляя свободное место между командами. (Особая точность с эмодзи не нужна.) 3. В правой части листа запишите каждый из ответов «Чтоделает» рядом с командой, с которой он соединен. Прочитайте оба столбца — код должен постепенно приобретать смысл. Создаеткопииэмодзи,переставляетих в случайном порядкеи сохраняет вспи- скеshuffledAnimals. Создает новый генератор случайных чисел. Создаетсписок из восьмипар эмодзи. Настраивает игру при каждой перезагрузкестраницы. Конец метода,выполняю- щего настройку игры. Создаетвторой список для хр а нения п ер еста вл енных эмодзи. Начало метода, выполняющего настройку игры. Кто и что делает? ае ааеееееееееееееееее Возьми вруку карандаш M I N I ре ш ение
дальше 4 727 начинаем программировать на C# ¢ Visual Studio — интегрированная среда разработки (IDE) компанииMicrosoft, которая упрощает редакти- рование файлов с кодом C# и выполнения различных операций с ними. ¢ Консольные приложения .NET Core — кросс- платформенные приложения с текстовым вводом и выводом. ¢ Приложения Blazor WebAssembly позволяют стро- ить полнофункциональные интерактивные веб- приложения из кода C# и разметки HTML. ¢ Функция IDE IntelliSense помогает быстрее и точнее вводить код. ¢ Visual Studio может выполнять приложения Blazor в режиме отладки, открывая браузер для отображения приложения. ¢ Пользовательские интерфейсы приложений Blazor строятся в формате HTML — языке разметки для по- строения веб-страниц. ¢ Razor позволяет включать код C# прямо в разметку HTML.Файлы страниц Razor имеют расширение .razor . ¢ Используйте синтаксис @ для встраивания кода C# в страницуRazor. ¢ Цикл foreach в страницах Razor повторяет блок кода HTML для каждого элемента в списке. Я не уверена насчет этих упражнений «Возьми в руку карандаш» и с соединением ответов. Разве не лучше просто дать мне код, который можно ввести в IDE? Отработка навыков понимания кода повысит вашу квалификацию разработчика. Письменныеупражнения являются обязательными. Они предоставляют вашемумозгуновый способусвоенияинформа- ции. Однако ониделаютнечто ещеболееважное: онипредо- ставляютвамвозможность ошибаться.Ошибки—важнаячасть обучения,и мытожедопустили множество ашибок(возможно, вы даже найдете одну-двеопечаткив этойкниге!). Никто не пишет идеальный код с первогораза—действительнохорошие программистывсегдапредполагают, чтонаписанныйсегодня код, возможно, потребуетсяизменить завтра. Позднее вкниге вы узнаете орефакторинге — приемепрограммирования, су- тью которого становитсяулучшение вашего кода после того, каконбудетнаписан. В таких списках «ключевых момен- тов» приводятся краткие сводки многих идей и средств, которые были представлены в этой главе. КЛЮЧЕВЫЕ МОМЕНТЫ
728 Visual Studio для пользователей Mac управление версиями Добавление нового проекта в систему управления версиями В этой книге мы будем строить многоразных проектов. Только представьте, какудобнобылобыиметьместо, вкотором ихможно было бы сохранить или загружать позднее с любогоустройства? А есливы допустите ошибку,разве небудет удобно вернуться к предыдущейверсиивашего кода?Что ж,вамповезло!Именно эту задачурешаетсистемауправления версиями:она предоставляет простые средства для создания резервной копии всего кода и от- слеживания всех вносимых изменений. Visual Studio позволяет легко добавлять проекты в системууправления версиями. Git — популярная система управления версиями, и Visual Studio можетпубликовать ваш исходный код в любом репозиторииGit. МысчитаемGitHub одним из самых удобных провайдеровGit. Для сохранениякода вампонадобитсяучетнаязаписьGitHub.Еслиувас ещенет учетнойзаписи,зайдитена сайтhttps://github.com и создайте ее. Когда ваша учетнаязаписьGitHubбудетнастроена,выможетевоспользоваться встроеннымисредства- миуправленияверсиями вашейIDE.Выберитекоманду VersionControl>>PublishinVersionControl, чтобы открыть окноCloneRepository: Включать проект в си- стему управления вер- сиями не обязательно. Возможно, вы работаетена ком- пьютереофиснойсети,которая не имеет доступа кGitHub(реко- мендованному нами провайдеру Git).Аможетбыть, вам простоне хочется этого делать. Какбы то ни было,этот шаг можно пропу- стить —илиже опубликовать код в приватномрепозитории, если вы хотите хранить резервную копию,но нехотите, чтобы она была доступна для других. В документации Visual Studio for Mac приведено полноеруководство по созданию проектов наGitHub и публикации их из Visual Studio. Оно включает пошаговые инструкции для создания удаленного репозитория на GitHub и публикации проектов в репозитории Git непосредственно из Visual Studio. Мы считаем, что все ваши проекты стоит публиковать наGitHub — это позволит вам легко вернуться к ним в будущем. https://docs.microsoft.com/en- us/visualstudio/mac/set-up-git-repository После того как вы создадите удаленный репозиторий на GitHub, вы сможете вставить сюдаURL-адрес. Введите свое имя по ль зо ват еля. РЕЛАКС
дальше 4 729 начинаем программировать на C# вы находитеСь здеСь Создание проекта Случайная раССтановка животных обработка щелчков Проверка Победы игрока Добавление та йм ера Добавление кода C# для обработки щелчков Мысоздаликнопки со случайными эмодзи. Теперь необходимо сделать так, чтобыэти кнопкиреагировали нащелчки пользователя. Воткакэтоделается: Игрок ще лка ет на пе рвой кнопке . Игрок щелкает на парах кнопок. Когда он щел- кает на первой кнопке, игра сохраняет инфор- мацию о животном,связанном с этойкнопкой. Игрок щелкает на второй кнопке. Играопределяет животное навторойкноп- кеисравнивает егос животным,сохранен- ным припервом щелчке. Игра проверяет совпадение. Еслиживотныесовпадают, игра перебирает все эмодзи в списке. Она находит в списке эмодзи, соответствующие найденной паре, и заменяет их пустыми строками. Если животныене совпадают, игра ничего не де ла ет. В любом случае последнее найденное животное сбрасывается, чтобы повторить процедуру для следующего щелчка.
730 Visual Studio для пользователей Mac обработка щелчков Назначение обработчиков щелчков кнопкам Когда вы щелкаете на кнопке,приложениедолжно что-то сделать.В веб-страницахщелчокявляетсясо- бытием. У веб-страниц также существуют и другиесобытия, например завершение загрузки страницы илиизменениеввода.Обработчиксобытия представляет собой код C#,которыйвыполняется каждый разпривозникновенииопределенного события. Мыдобавимобработчиксобытия,реализующийфунк- циональность кнопки. Код обработчика события Добавьте следующий код в страницу Razor, непосредственно перед закрывающей фигурной скобкой } внижнейчасти: stri ng la stAni malFo und = stri ng.Em pty; priv ate v oid B utton Click (stri ng an imal) { if (l astAn imalF ound == st ring. Empty ) { / / Выб ор пе рвого элем ента пары. Запом нить его. l astAn imalF ound = ani mal; } else if (l astAn imalF ound == an imal) { / / Сов паден ие об наруж ено! Выпол нить с брос для с ледую щей п ары. l astAn imalF ound = str ing.E mpty; // Найде нные живот ные з аменя ются пусты ми ст рокам и, чт обы с крыть эмодз и на экран е. s huffl edAni mals = shu ffled Anima ls .S elect (a => a.Re place (anim al, st ring. Empty )) .T oList (); } else { / / Пол ьзова тель выбра л неп арных живот ных. / / Сбр осить сохр аненн ый вы бор. l astAn imalF ound = str ing.E mpty; } } Связывание обработчиков событий с кнопками Теперь необходимо внестиизменения,чтобы прищелчкахна кнопках вызывался метод ButtonClick: @foreach (var animal in animalEmoji) { <div class="col-3"> <button @onclick="@(() => ButtonClick(animal))" type="button" class="btn btn-outline-dark"> <h1>@shuffledAnimals</h1> </button> </div> } Когда мы предлагаем вам обновить что-то в блоке кода, остальной код выделяется более светлым, а изменяемый код — жирным шрифтом. Включите атрибут @onclick в размет- ку HTML в foreach. Будьте внимательны с круглыми скобками. Строки с // содержат ком- ментарии. Они ничего не делают — комментарии приводятся только для того, чтобы код стал более понятным. Мы включили их дляудобочитаемости кода. Это запрос LINQ. Техно- логия LINQ более подробно рассматривается в главе 9. Не беспокойтесь, если вы не на сто процентов понимаете, что делает этот код C#! Пока сосредоточьтесь на том, чтобы ваш код полностью совпадал с приведенным.
дальше 4 731 начинаем программировать на C# Разберемся с тем, как работают обработчики событий. Мы связали код из обработчика события с предшествующи- ми объяснениями того, как игра обнаруживает щелчки. Взгляните на следующий код и сравните его с кодом, только что введенным в IDE. Попробуйте понять его логику — ничего страшного, если что -то останется непонятным , до- статочно составить общее представление о том, как добавленные фрагменты сочетаются друг сдругом. Это упраж- нение развивает ваши навыки понимания кода C#. Обработчик события под увеличительным стеклом В этом коде присутствует ошибка! А вы сможете ее обнаружить? Мы найдем ошибку и исправим ее в следующем разделе Игрокщелкает на первой кнопке. Код проверяет, является ли эта кнопка первой, на которой был сделан щелчок. В таком случае ин- формация о животном, связанном с кнопкой, со - храняется в lastAnimalFound. Игрокщелкает на второй кнопке. Команды между открывающей { изакрывающей } фигурной скобкой выполняются только в том слу- чае, если игрок щелкнул на кнопке с животным, совпадающим с тем, которое было связано с пер- вой кнопкой. Игра проверяет совпадение. Код C#выполняется только в том случае, если вто - роеживотное совпадает с первым. Он перебирает список эмодзи и заменяет элементы, совпадающие с текущей парой, пустыми строками. if (lastAnimalFound == string.Empty) { lastAnimalFound = animal; } else if (lastAnimalFound == animal) { } shuffledAn imals = sh uffledAnim als .Select (a => a.Re place(anim al, string .Empty)) .ToLis t(); lastAnimalFound = string.Empty; Эта команда встречается в коде дважды: в части, которая выполняется, если второе животное со- впадает с первым, и в части , выполняемой при несовпадении. Эмодзи последнего найденного животного заменяется пустой строкой, так что следующий щелчок на кнопке зафиксирует первое животное в паре.
732 Visual Studio для пользователей Mac ошибка в коде Тестирование обработчика события Сновазапуститеприложение.Когдастраница появится наэкране,протестируйтеобработчиксобытия: щелкнитенакнопке,а затем щелкнитена кнопкес парнымэмодзи. Оба изображениядолжныисчезнуть со страницы. Щелкнитена другойпаре,затем наследующейит. д. Приложениепозволяет щелкать на парах,покавсе кнопки неостанутся пустыми. Поздравляем,вы нашли все пары! Но что произойдет, если щелкнуть на одной кнопке дважды? Перезагрузитестраницу вбраузере, чтобы сбросить игру.Но на этотраз вместо того, чтобы выбрать пару,снова щелкнитена тойжекнопке. В игреобнаруживаетсяошибка!Этотщелчок должен быть про- игнорирован,но вместо этого программаработает так,словно было найдено совпадение. Если снова щелк­ нуть на той же кнопке, игра дей- ствует так, слов- но вы нашли совпа- дение. Но такого быть не должно!
дальше 4 733 начинаем программировать на C# Диагностика проблемы в отладчике У каждой ошибки есть объяснение — в программе ничего не происходит без причин, но не каждую ошибку легко обнаружить. Пониманиеошибки— первый шаг к ееисправлению. Ксчастью,дляэтого существует превосходныйинстру- мент — отладчик VisualStudio. Подумайте, что могло пойти не так. Первое, на чтоследует обратитьвнимание, — что ошибка воспроизводится:каждыйраз, когда выповторно щелкаетенаоднойкнопке,программаработаеттак,словновы щелкнулина паре. Второйважныймомент: выдостаточнохорошо представляете, где находится ошибка.Пробле- ма возникла только послетого,как выдобавиликодобработки событияClick,так что начинать следует именно с него. Добавьте точку прерывания в только что написанный код обработчика события Click. Щелкните в первой строке метода ButtonClick и выберите команду меню Run>>Toggle Breakpoint (⌘\).Строка изменяет цвет,а на левыхполях появляется точка: 1 2 Когда ваше приложение приостанавливается в отладчике (это называется «прерыва- нием»), на панели инструментов появляются элементы отладки. В книге вы еще не раз потренируетесь в их использовании, поэтому запоминать их назначение сейчас не обязательно. Пока просто прочитайте описания, наведите указатель мыши на каж- дый элемент, чтобы увидеть подсказку, и найдите соответствующие сочетания кла- виш в меню Run(например, ⇧⌘O для Step Over). Кнопка Contunue Execution переза- пускает приложе- ние. Кнопка Pause Execution приостанавливает приложение. Кнопка Step Into выпол- няет следующую команду. Если эта команда является вызовом метода, то вы- полняется только первая команда в этом методе. Кнопка Step Over также вы- полняет следующую команду, но если эта команда является вызовом метода, то будет выполнен весь метод в целом. Кнопка Step Out завер- шает выполнение теку- щего метода и прерыва- ет программу в строке, следующей за той, из которой он был вызван. Анатомия отладчика Когда в строке уста- навливается точка прерывания, IDE меняет цвет фона и отображает точку на полях слева.
734 Visual Studio для пользователей Mac сб ор д ок азател ьств Отладка обработчика события Послеустановки точки прерывания мы сможем составить некоторое представление о том, что же по- шло не такв вашем коде. Щелкнитенакнопкесживотным, чтобы активизировать точку прерывания. Еслиприложениеужевыполняется,остановите его изакройтевсеокнабраузера. Затем снова запуститеприложениеищелкните на любойкнопке с животным.Среда VisualStudio активизи- руется, а ееокновыходит на переднийплан.Строка, вкоторойустановлена точкапрерывания, выделяется другим цветом: Переместитеуказатель мыши к первой строке метода (начинающейся со слов private void)инаведите указатель мыши на animal. На экране появляется маленькоеокно с животным,на которомбыл сделанщелчок: Нажмите кнопку Step Over или выберите команду меню Run>Step Over (⇧⌘O). Цветовое вы- делениепереходитк строке{.Снова выполнитету жекоманду,чтобыпереместить выделение к следующей команде: Повторите команду пошагового выполнения еще раз, а затем наведите указатель мыши на lastAnimalFound: Выполненнаякоманда задает значение переменнойlastAnimalFound, чтобы оно соответство- вало animal. Таквкодесохраняется первоеживотное, на котором щелкнул игрок. Продолжите выполнение. Нажмите кнопку Continue Executionили выберите команду меню Run>>Continue Debugging (⌘↩).Переключитесь обратновбраузер — игра продолжитвыполнятьсяс точкипрерывания. 3 4 Наведите ука- затель мыши на «animal», чтобы увидеть эмодзи, на котором был сделан щелчок.
дальше 4 735 начинаем программировать на C# Щелкните напарном животном Найдите кнопку с парным эмодзи и щелкните на ней.IDE активизируетточку прерывания иснова приостанавливаетприложение.НажмитекнопкуStepOver— первыйблокпропускается, иуправлениепередаетсявторому: Совпадение обнаружено! Выполнить сброс для следующей пары Наведите указатель мышинаlastAnimalFound иanimal — вобеих переменных должны хра- нитьсяодинаковыеэмодзи.Такобработчиксобытияузнает онайденномсовпадении.Выполните команду StepOver ещетрираза. Найденные животные заменяются пустыми строками. Теперь наведите указатель мыши на shuffledAnimals. В открывшемся окне перечислено несколько вариантов. Щелкните на треугольникерядом с shuffledAnimals,чтобыраскрыть раздел, а затем раскройте_items,чтобы просмотреть всехживотных: НажмитеStepOver еще раз,чтобы выполнить командуудалениясовпаденийиз списка.Затем снова наведитеуказательмышина shuffledAnimalsипросмотрите элементы. Срединихдва пустых (null)значения там, где находились совпадающие эмодзи: 5 После того как вы рас- кроете shuffledAnimals и _items, используйте отладчик для просмотра содержимого List. О том, что такое списки List и как они работают, рас- сказано в главе 8. Мы проанализировали большой объем доказательств и нашли важные улики. Как вы думаете, что стало причиной проблемы? shuffledAnimals представляет собой список List со всеми живот- ными, находящи- мися в игре на данный момент. Используйте кнопки с тре- угольниками, чтобы снача- ла раскрыть shuffledAnimals, а затем раз- верните _items, чтобы увидеть содержащиеся в них элементы.
736 Visual Studio для пользователей Mac исправление ошибки Поиск ошибки, породившей проблему... Пришловремяпобывать вролиШерлокаХолмса. Мы собралинемалоулик. Вот что нам известно на данныймомент: 1. Каждыйраз,когда вы щелкаетена кнопке, выполняется обработчиксобытия. 2. Обработчиксобытияиспользует animal дляопределения того, на какомживот- ном вы щелкнулипервым. 3. ОбработчиксобытияиспользуетlastAnimalFoundдляопределениятого, накаком животном был сделан второй щелчок. 4. ЕслизначениеanimalравноlastAnimalFound, обработчикделает вывод, что найдено совпадение, и удаляет соответствующих животных из списка. Что же произойдет, если щелкнуть на одной кнопке с животным дважды? Давайте узнаем! Повторите те же действия, которые выполнялись ранее, но на этот раз щелкните на одном животном дважды. Посмотрите, что произойдет на шаге (5). Наведитеуказатель мыши на animal иlastAnimalFound, как это делалось ранее. Они совпадают! Это происходит из-затого, чтоб обработчик неразличаетразныекнопки с одинаковыми животными. . .. и ее и справление! Теперь, когдамызнаем, из-зачего происходит ошибка, становится ясно, какее исправить: нужно научить обработчик событияразличать двекнопки с одинаковыми эмодзи. Дляначала внеситеследующие изменения в обработчик ButtonClick (будьте внимательны и не про- п ус тите к ак ое- нибудь изм енение): string lastAnimalFound = string.Empty; string lastDescription = string.Empty; private void ButtonClick(string animal, string animalDescription) { if (lastAnimalFound == string.Empty) { // Выбор первого элемента пары. Запомнить его. lastAnimalFound = animal; last Des crip tio n = ani mal Des crip tio n; } else if ((lastAnimalFound == animal) && (animalDescription != lastDescription)) Заменим цикл foreachдругойразновидностью:циклом for —он подсчитывает животных: < div cla ss= "row "> @for (var animalNumber = 0; animalNumber < shuffledAnimals.Count; animalNumber++) { var an imal = shuf fle dAn imal s[a nim alNu mbe r]; var un ique Des crip tio n = $"B utt on #{an ima lNu mber }"; <div class="col-3"> <button @onclick="@(() => ButtonClick(animal, uniqueDescription))" type="button" class="btn btn-outline-dark">@animal</button> Теперь снова включитеотладку приложения,какделалось ранее. На этотраз, если щелкнуть на одном животном дважды,управлениебудетпередано в конец обработчика события. Ошибка исправлена! По следу Теперь каждая кнопка получает не только эмодзи с животным, но и описание, а обработ- чик события использу- ет lastDescription для отслеживания описания. Проверяем, что совпадают как животные, так и описания. Замените цикл foreach циклом for. Циклы рас- сматриваются в главе 2.
дальше 4 737 начинаем программировать на C# В: Вы упоминали о запуске сервера и веб-приложения. Что имеетсяв виду? О: Когда вы запускаете свое приложение, IDE запускает вы- бранный вамибраузер.В адресной строкебраузера выводится URL-адрес вида https://localhost:5001/ — если скопировать URL-адресивставитьеговадресную строкудругогобраузера, в этом браузере такжебудет запущена ваша игра. Дело в том, чтов браузеревыполняетсявеб-приложениеиливеб-страница, которая выполняется исключительно в браузере. Как и любая веб-страница, она должнаразмещаться на веб-сервере. В: Ккакому веб-серверу подключается мой браузер? О:Вашбраузерподключаетсяк серверу,работающему внутри Visual Studio. Щелкните на кнопке Application Output в нижней части IDE, чтобы открыть окно с результатом выполняемого приложения, — в данном случае это приложение, включающее сервер, который управляет вашим веб-приложением. Про- крутите окно или найдите в нем строку, в которой выводится информация опрослушиваниивходных подключенийбраузера: Now listening on: https://localhost:5001 В:Когда я нажимаю⌘⇥(Command+Tab)для переключе- ния междуприложениями macOS, в системеобнаруживаются открытые экземпляры Edge или Chrome. Что происходит? О:Каждыйраз, когда вы останавливаете и перезапускаете свое приложение в Visual Studio, запускается новый экземплярбраузе- ра, потому чтодля отладкидолжно устанавливаться отдельное подключение. Вы можете подключаться кдругимэкземплярам брау зер а, но отл адка в озмож на толь ко в брау зере, зап у щенном IDE.Вы можете убедиться в этом сами: запустите, остановите и перезапустите свое приложение в IDE, затем установите точку прерывания.Только один избраузеровдействительнобудет при- остановлен придостижении точки прерывания. В: Веб-приложенияBlazor выглядят намного сложнее кон- сольныхприложений.Они действительноработают так же? О: Да. Если разобраться, весь код C# работает по одному принципу: сначалавыполняется однакоманда,потомследующая, потом следующая за ней и т. д. Одна из причин, по которым веб-приложения кажутся более сложными, заключается в том , что некоторые методы вызываются только при возникновении определенных событий, например при загрузке страницы или щелчкена кнопке. После того как методбудет вызван, он рабо- тает точно так же, как в консольном приложении, и вы можете убедиться в этом, установив в нем точку прерывания. Если только вы не обладаетесверхъестественнойспособностью идеально программировать без единой ошибки,выужеви- делиокноErrorsвнижнейчасти IDE.Онооткрываетсяпри попыткезапускапроекта, содержащегоошибки. Воткаквыглядело этоокно, когдамыпопыталисьисправитьошибку,нослучайнодопустилиопечатку:string lsatDescription = string. Empty; Наличие ошибок всегда можно проверить, построив ваш код: либо запустите его, либо выберите команду Build All (⌘B) вменю Build.ЕслиокноError непоявится,значит, код былопостроенуспешно; этонеобходимоIDEдляпреобразованиякода в двоичную форму, т.е. исполняемыйфайл,который может бытьзапущен системой macOS. Давайте намеренно включим ошибку в код. Перейдите в первую строку метода SetUpGame, а затем введите в отдельной строкесимволыXyz. Попробуйтепостроить ваш код. IDEоткрывает окно Errors с сообщением в верхней части и содной ошибкой. Если щелкнутьв другом месте, окно Errors исчезнет, но не беспокойтесь, вы всегда можете открыть его, щелкнув на кнопке в нижнейчасти IDE. Обращайте внимание на врезки с вопросами и ответами. Они часто дают ответы на самые насущные вопросы, а так - же поднимают другие вопросы, которые могли прийти в голову другим читателям. Собственно, многие из них — вполне реальные вопросы от читателей предыдущего издания этой книги! часто Зад авае м ые вопросы
738 Visual Studio для пользователей Mac нахождение всех животных и сброс игры вы находитеСь здеСь Соз- да ние проекта Случайная раССтановка животных Обра- бОтка щелчкОв проверка победы игрока Добав- ление та йме ра Проанализируйтекод C# иразметку HTML. Как вы думаете, какие части необходимо изменить, чтобы вернуть игру в исходное состояние, когда игрок найдет все совпадающие пары? Когда вы встречаете врез- ку «Мозговой штурм», как следует поразмыслите над заданным вопросом. Добавление кода для сброса игры при победе Игра работает так, как положено, — сначала игроку предоставляется сетка, заполненная животными. Он щелкает на парах животных,исчезающих при успешном составлении пары. Но что произойдет, когда будут найдены все совпадения?Необходимокаким-то образомсброситьсостояниеигры,чтобы игрок мог сделать следующую попытку. Игрок щелкает на парах, найденные пары исчезают Рано или поздно игрок нах одит все пары Когда будет найдена после дня я пара , игра возвращается в исход ное с ост оя ние Мозговой штурм
дальше 4 739 начинаем программировать на C# Ниже приведены четыре блока кода, которые следует включить в приложение. Когда все блоки окажутся на своих местах, играбудет возвращаться в исходное состояние после того, как игрок найдет все пары. m atch esF oun d++; if (matchesFound == 8) { SetU pGa me( ); } int matchesFound = 0; matchesFound = 0; < div cla ss= "row "> <h 2>M atc hes fou nd: @ma tch esFo und </h 2> </div> Вашазадача — определить, где следует разместить все четыреблока. Мы скопировали ча- сти кода и добавили четыре прямоугольника, по одному для каждого блока. Как определить, вкаком прямоугольникедолжен находиться каждый из четырехблоков? <div class="con tainer"> <div c lass="row" > @f or (var an imalNumber = 0; anim alNumber < shuffledA nimals.Cou nt; animal Number++) { var anim al = shuff ledAnimals [animalNum ber]; var uniq ueDescript ion = $"Bu tton #{ani malNumber} "; <div cla ss="col-3" > <but ton @oncli ck="@(() = > ButtonCl ick(animal , uniqueDe scription) )" type=" button" cl ass="btn b tn-outline -dark"> <h1>@anima l</h1> </bu tton> </div> } </div> </div > List<str ing> shuff ledAnimals = new Lis t<string>( ); privat e void Set UpGame() { Random random = new Random (); shuffl edAnimals = animalEm oji .O rderBy(ite m => rando m.Next()) .T oList(); } else i f ((lastAn imalFound == animal) && (anima lDescripti on != last Descriptio n)) { // Совпадени е обнаруже но! Выполн ить сброс для следую щей пары. la stAnimalFo und = stri ng.Empty; // Найденные животные заменяются пустыми с троками. sh uffledAnim als = shuf fledAnimal s .Select( a => a.Rep lace(anima l, string. Empty)) .ToList( ); } Это упражнение не предназначено для вы- полнения «на бумаге» — оно выполняется изменением кода в IDE. Когда вы видите над- пись Упражнение с кроссовками, это означа- ет, что вы должны вернуться в IDE и присту- пить к написанию кода C#. Какой из четырех блоков кода размещается в этом блоке? Когда вы выполняете упражнение по програм- мированию, помните: подсматривать в реше- ние — это не жульниче- ство! Раздражение не способствует эффектив- ному обучению — все мы легко можем споткнуться на какой-то мелочи, а ре- шение поможет вам спра- виться с препятствием. Упражнение
740 Visual Studio для пользователей Mac з авершен ие иг ры Ниже показано, как будет выглядеть код, когда все блоки окажутся на своих местах. Добавьте все четыре блока в игру, если это еще не было сделано ранее, чтобы игра автоматически возвращалась в исходное состояние при обнаружении игроком всех совпадений. < div class ="containe r"> <d iv class=" row"> @for (va r animalNu mber = 0; animalNumb er < shuff ledAnimal s.Count; a nimalNumbe r++) { var animal = s huffledAni mals[anima lNumber]; var uniqueDesc ription = $"Button # {animalNum ber}"; <div class="co l-3"> <button @o nclick="@( () => Butt onClick(an imal, uni queDescrip tion))" ty pe="button " class="b tn btn-out line-dark "> <h1>@a nimal</h1> </button> </di v> } </ div> <div cl ass ="ro w"> < h2> Mat ches fo und: @m atc hesF oun d</ h2> </di v> < /div> List <string> s huffledAni mals = new List<stri ng>(); int matchesFound = 0; pr ivate void SetUpGame () { Ra ndom rando m = new Ra ndom(); sh uffledAnim als = anim alEmoji .OrderBy (item => r andom.Next ()) .ToList( ); matchesFound = 0; } el se if ((la stAnimalFo und == ani mal) && (a nimalDescr iption != lastDescr iption)) { // Совпа дение обна ружено! Вы полнить сб рос для сл едующей п ары. lastAnim alFound = string.Emp ty; // Найде нные живот ные заменя ются пусты ми строкам и. shuffled Animals = shuffledAn imals .Sel ect(a => a .Replace(a nimal, str ing.Empty) ) .ToL ist(); ma tch esFo und ++; if (matchesFound == 8) { SetUpGame(); } } Вы достигли очередной контрольной точки вашего проекта! Возможно, игра еще не закончена, но она рабо- тает. Самое время сделать шаг назад и подумать над тем, как улучшить программу. Какие изменения выбы предло- жили для того, чтобы сделать ее более интересной? Эта разметка Razor использу- ет @matchesFound для того, что- бы страница выводила количество найденных совпадений под сеткой с кнопками. Здесь игра хранит количество совпадений, найденных игроком на данный момент. При настройке или сбросе игры количество найденных совпадений обнуляется. Каждый раз, когда игрок находит пару, этот блок увеличи- вает matchesFound на 1. Если найдены все 8 совпадений, игра возвращается в исходное состояние. Упражнение Реше н ие Мозговой штурм
дальше 4 741 начинаем программировать на C# Создание проекта Случайная раССтанов- ка живот- ных Обра- бОтка щелчкОв Проверка Победы игрока добавление та йм ера вы находитеСь здеСь Т и к Т и к Тик Таймер срабатывает по- сле истечения заданного интервала, а назначен- ный метод вызывается снова и снова. Мы ис- пользуем таймер, кото- рый запускается вместе с запуском игры и пере- стает работать после нахождения последнего животного. Добавление таймера Нашаигра станетболееинтересной,еслиигроксможет попытаться побить свойрекорд. Добавим таймер,который срабатывает сфик- сированным интервалом имногократно вызываетметод. Сделаем игру чуть более азартной! В нижней части окна выводится вре- мя, прошедшее с момента запуска игры. Показания таймера постоянно увели- чиваются, а останавливается таймер только после нахождения последней пары.
742 Visual Studio для пользователей Mac добавление таймера Добавление таймера в код игры Сначала найдите следующую строку в верхней частифайла Index.razor: @page "/ ": Добавьтеследующуюстроку подней —это необходимо дляиспользования таймера вкоде C#: @using System.Timers Далеенеобходимо обновитьразметкуHTMLдлявывода времени.Вставьте следующийфрагмент сразу же после первогоблока, добавленного вупражнении: </div> <div class="row"> <h2>Matches found: @matchesFound</h2> </d iv> <div class="row"> <h2>Time: @timeDisplay</h2> </div> </div> В страницу нужно добавить таймер.Крометого, понадобится переменная для хранения за- траченного времени: List<string> shuffledAnimals = new List<string>(); int matchesFound = 0; Timer timer; int tenthsOfSecondsElapsed = 0; string timeDisplay; Таймерунеобходимо сообщить, с какойчастотойондолжен срабатыватьикакойметоддолжен вызываться. Это будет происходить в методе OnInitialized, который вызывается однократно послезагрузкистраницы: protected override void OnInitialized() { timer = new Timer(100); timer.Elapsed += Timer_Tick; SetUpGame(); } Обнулитезатраченноевремя при инициализацииигры: private void SetUpGame() { Random random = new Random(); shuffledAnimals = animalEmoji .OrderBy(item => random.Next()) .ToList(); matchesFound = 0; tenthsOfSecondsElapsed = 0; } 1 2 3 4 5 Добавьте
дальше 4 743 начинаем программировать на C# Таймер необходимо останавливать и запускать. Добавьте следующую строку в начало метода ButtonClick, чтобытаймер запускалсяпри щелчкена первой кнопке: if (lastAnimalFound == string.Empty) { // Выбор первого элемента пары. Запомнить его. lastAnimalFound = animal; lastDescription = animalDescription; timer.Start(); } Добавьте следующие две строки в метод ButtonClick, чтобы остановить таймер и вывести со- общение «PlayAgain?» послетого,как игрок найдетпоследнеесовпадение: matchesFound++; if (matchesFound == 8) { timer.Stop(); timeDisplay += " - P lay Again?"; SetUpGame(); } Наконец, таймер должен знать, что делать при каждом срабатывании. Подобно тому как у кнопок есть обработчики событий Click, у таймера есть обработчик события Tick, который выполняется прикаждом срабатываниитаймера. Добавьтеследующийкод вконецстраницы, непосредственнопередзакрывающейфигурной скобкой}: private void Timer_Tick(Object source, ElapsedEventArgs e) { InvokeAsync(() => { tenthsOfSecondsElapsed++; timeDisplay = (tenthsOfSecondsElapsed / 10F) . ToString("0 .0s"); StateHasChanged(); }); } 6 7 Таймер запускается, когда игрок щелкает на первом животном, и останавливается при обнаружении последнего совпадения. Таймер не изменяет игровой процесс, но делает его более интересным.
744 Visual Studio для пользователей Mac работа подошла к концу Очистка меню навигации Ваша играработает!Но вы заметили, что в приложении также присутствуютдругиестраницы? Попро- буйтещелкнутьна ссылках «Counter» или «Fetchdata» внавигационномменю слева.Когда вы создавали проектBlazor WebAssembly App, среда Visual Studio добавила дополнительные страницы. Их можно безопасно удалить. Дляначалараскройтепапку wwwroot и отредактируйтефайлindex.html.Найдитестроку, которая начи- нается с тега <title>, и измените ее, чтобы привести к виду <title>Animal Matching Game</title>. Раскройте папкуShared врешении и сделайте двойной щелчок на файлеNavMenu.razor . Найдите следующую строку: <a class="navbar-brand" href="">BlazorMatchGame</a> изамените ее следующей: <a class="navbar-brand" href="">Animal Matching Game</a> Затем удалите следующие строки: <li class="nav-item px-3"> <NavLink class="nav-link" href="counter"> <span class="oi oi-plus" aria-hidden="true"></span> Counter </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="fetchdata"> <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data </NavLink> </li> Наконец, удерживая нажатой клавишу⌘ (Command), выделите несколько файлов, щелкая на них вокнеSolution: Counter.razor и FetchData.razor впапкеPages,SurveyPrompt.razor впапкеShared, а такжевсю папку sample-data впапке wwwroot. Когда файлыбудутвыделены,щелкните правой кнопкоймыши на любом из них ивыберитекоманду менюDelete (⌘⌫),чтобыудалить их. Итеперьигра готова! Да, это очень удобно — разбить игру на меньшие части, с которыми можно справиться поочередно. Любой крупный проект всегда рекомендуется разбивать на меньшие части. Один из самых полезных навыков программирования — умение взглянуть на большую исложную задачу иразбить еенаряд меньших,легко решаемыхзадач. Всамомначалебольшого проекта легковпасть вуныние:«Ого, даэто бесконечная работа!»Но есливы сможетевыделить меньшую подзадачуиначнететрудиться надней,этостанетотправной точкойдляработы. А когда эта часть будетзавер- шена,можноперейтик следующейменьшейчасти,потом кследующейит.д. Во времяпостроениякаждой части выбудете всебольшеузнавать опроекте в целом.
дальше 4 745 начинаем программировать на C# ¢ Обработчик события представляет собой метод, который вызывается вашим приложением при воз- никновении некоторого события (щелчка кнопкой мыши, перезагрузки страницы, срабатывания тай- мераит.д.). ¢ В окне Errors в IDE выводятся описания ошибок, которые препятствуют построению вашего прило- жения. ¢ Таймеры многократно выполняют обработчик со- бытия Tick с заданным интервалом. ¢ foreach — разновидность циклов для перебора коллекций элементов. ¢ for — разновидность цикла, хорошо подходящая дляотсчета фиксированного количества элементов. ¢ Когда в вашей программе происходит ошибка, собе- рите информацию и постарайтесь определить, что является ее причиной. ¢ Воспроизводимые ошибки проще устранять. ¢ Отладчик IDE позволяет приостановить приложе- ние на конкретной команде, чтобы найти потенци- альные проблемы. ¢ При установке точки прерывания отладчик при- останавливает выполнение на команде, в которой она была установлена. ¢ Visual Studio упрощает использование систем управления версиями для резервного копирова- ния кода и контроля вносимых вами изменений. ¢ Вы можете сохранять свой код в удаленном репо- зитории Git. Мы создали репозиторий с исходным кодом всех проектов в книге на GitHub. Еще лучше, если... Игра получилась вполнедостойной!Но любую игру— да, собственно, практически любую программу — можноусо- вершенствовать. Несколько предложений,которые,как нам кажется,моглибыулучшить нашу игру: ‘ Добавьтебольше видовживотных, чтобывигре не использовались одниите же изображения. ‘ Хранителучшеевремя, чтобыигрокмог попытать- сяпобить свойрекорд. ‘ Реализуйте обратныйотсчетвремени, чтобывремя игрокабыло ограничено. О т л и чная р а б о т а ! Мы абсолютно серь езно: не пожалейте времени и сделайте это. Когда вы дела- ете шаг назад и размышляете о только что завершенном проекте, это отлично помогает закрепить в мозге только что усвоенный материал. На всякий случай напомним: в книге мы часто называем Visual Studio просто «IDE». Самое время сохранить ваш код в Git! Вы всегда сможете вернуться к своему проекту, если захотите повторно использовать какую­то часть его кода. КЛЮЧЕВЫЕ МОМЕНТЫ А сможете ли вы предложить соб- ственные улучшения для игры? Это очень полезное упражнение — поду- майте несколько минут и запишите не менее трех улучшений для игры с по- иском пар. M I N I Возьми в руку карандаш
746 Visual Studio для пользователей Mac каждый пользовательский интерфейс имеет свою механику Близкое знакомство с c# Из главы 2 В последней части главы 2 был приведен проект для экспериментов с разными типами элементов управления в Windows. Мы воспользуемся Blazor, чтобы построить аналогичный проект для экспериментов с элементами управления. Элементы управления определяют механики ваших пользовательских интерфейсов Впредыдущейглаве дляпостроенияигрыиспользовались элементыуправленияButton.Однакосуществует многоразныхспособов использованияэлементов,а решения, принятые вами привыборе элементов, могут оченьсильноизменить вашеприложение.Звучит неожиданно?На самомделевсеэтооченьпохоженаприня- тиерешений припроектированииигр.Есливысоздаетенастольнуюигру,которойнуженгенераторслучайных чисел, выможете воспользоваться кубиками или картами.Если выработаете над игрой-платформером, вы можете решить, что игровой персонаж должен уметь прыгать, делать двойной прыжок, делать прыжок от стены или летать (или выполнятьразные действиявразное время).То же относится кприложениям:если выразрабатываетеприложение, вкоторомпользователь должен ввестичисло, выможете выбрать дляэтой цели разныеэлементы,и от вашеговыбора зависят впечатления пользователяприработе сприложением. Элементы управления — компоненты пользова- тельского интерфейса (UI), строительные блоки для построения UI. От выбора элемен- тов управления зависит механика вашего при- ло жени я. Позаимствуем идею механики из видеоигр. Она поможет лучше понять возможные варианты и принять правильные решения в любых приложениях (не только в видео- игр ах ). ‘ В текстовом поле пользователь может ввестилюбойтекстпо свое- муусмотрению.Приэтомвданном прим ере нео б хо дим о про верить, что пользователь вводит только числа,а не произвольный текст. ‘ Переключатели ограничивают выбор пользователя не- сколькими фиксированными вариантами. Они часто вы- глядят как кругисточкой внутри,нопри помощистилевого оформления им можно придать видобычныхкнопок. ‘ Ползунки предназначены исключительно для выбора чисел. В принципе телефонные номера тоже являются обычными числами, так что формально ползунок может использо- ватьсядлявыбора телефона.Как выдумаете, хорошая лиэто мысль? ‘ Селекторы — элементы управ- ления, специально разработан- ные для выбора определенного типа значений из списка. На- пример, селекторы даты по- зволяют задать дату с выбором составляющих года, месяца и даты, а селекторы цветов предоставляют возможность выбрать цвет припомощи пол- зунка,представляющего спектр, иливвести числовой код. Это Blazor­версия проекта настоль- ного приложения Windows из главы 2.
дальше 4 747 начинаем программировать на C# Создание нового проекта Blazor WebAssembly App РанеевэтомприложениимысоздалипроектBlazorWebAssemblyAppдляигры споиском пар.Теперь мы сделаем то же самоедляэтого проекта. Нижеподробно описана последовательностьдействийдля создания проектаBlazor WebAssembly App, изменения текстазаголовкаглавной страницыи удаления лишнихфайлов, созданныхVisual Studio. Мынебудем повторятьэту инструкциюдля всехостальныхпроектоввэтомруководстве— вы сможетесамостоятельно повторитьеедля всехбудущихпроектовBlazorWebAssemblyApp. Создайте новый проект Blazor WebAssemblyApp. Запустите Visual Studio 2019 for Mac или выберите команду меню File>>New Solution... (⇧⌘N), чтобы открыть окноNewProject. Щелкните на кнопке New, чтобы создать новыйпроект. Введите имяпроектаExperimentWithControlsBlazor. Измените заголовок и меню навигации. В конце проекта игры с поиском пар мы изменилитекстзаголовка и панелинавигации. Сделаем то же самое в этом проекте. Раскройте папку wwwroot и отредактируйте файл Index.html.Найдитестроку,которая начинаетсяс тега <title>,иизменитеее,чтобыпри- вести ее к виду <title>Experiment with Controls</title>. Раскройте папкуSharedврешенииисделайтедвойнойщелчок на файлеNavMenu.razor . Найдите следующую строку: <a class="navbar-brand" href="">ExperimentWithControlsBlazor</a> изаменитеееследующей: <a class="navbar-brand" href="">Experiment With Controls</a> Удалите лишние команды меню навигации и соответствующие файлы. Это тожесамое, что было сделано в конце проекта с поиском пар.Сделайте двойной щелчок на файле NavMenu.razor и удалите следующие строки: <li class="nav-item px-3"> <NavLink class="nav-link" href="counter"> <span class="oi oi-plus" aria-hidden="true"></span> Counter </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="fetchdata"> <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data </NavLink> </li> Наконец, удерживая нажатой клавишу⌘(Command), выделите несколько файлов, щелкая на них в окнеSolution: Counter.razor иFetchData.razor впапке Pages, SurveyPrompt. razor в папке Shared,а такжевсю папку sample-data впапкеwwwroot.Когда файлыбудут выделены, щелкните правойкнопкоймышина любомиз нихивыберите команду меню Delete (⌘⌫), чтобыудалить их. 1 2 3 близкое знакомство с c#
748 Visual Studio для пользователей Mac добавление страницы с ползунком Создание страницы с ползунком Многиепрограммы зависят от ввода чисел пользователем. Одним из самых популярных элементов управлениядляввода чиселявляется ползунок(slider).Создадим новую страницуRazor,использующую ползунок для обновления значения. Замените содержимое страницы Index.razor. ОткройтефайлIndex.razor изамените все его содержимое следующейразметкойHTML: @page "/" <div class="container"> <div class="row"> <h1>Experiment with controls</h1> </div> <div class="row mt-2"> <div class="col-sm -6"> Pick a number: </div> <div class="col-sm -6"> <input type="range"/> </div> </div> <div class="row mt-5"> <h2> Here's the value: </h2> </div> </div> Запустите приложение. Запуститеприложение, как это былосделановглаве 1.СравнитеразметкуHTMLсостраницей, ото- бражаемой вбраузере, — сопоставьте отдельные блоки <div>с тем, чтоотображаетсянастранице. 1 2 <div class="col-sm -6"> <input type="range"/> </div> <div class="row mt-5"> <h2>Here's the value:</h2> </div> <div class="row "> <h1>Experiment with controls</h1> </div> <div class="col-sm -6"> Pick a number: </div> Это тег элемента ввода. Его атрибут type определяет, какая разновидность элемента вводабудет отображаться на странице. Если присвоить type зна- чение range, отображается ползунок: <input type="range"/> Внешний вид элементов HTML ино- гда зависит от используемого брау- зера. Ползунок в Edge выглядит так: Отредактируйте страницу Razor — точно так же, как в игре с поиском пар из главы 1. Атрибут class="row" в этом теге приказы- вает странице сгенерировать весь контент, заключенный между открывающим тегом <div class="row"> и закрывающим тегом </div>, в одну строку на странице. Строка, которую мы вставили выше. Попро- буйте найти другие строки, измененные в процессе работы над приложением, в раз- метке HTML. Добавление mt­2 в атри- бут class до- бавляет над строкой верх- ний отступ.
дальше 4 749 начинаем программировать на C# Добавьте код C# в страницу. Вернитесь кфайлуIndex.razor идобавьте следующий код C# вконец файла: @code { private string DisplayValue = ""; private void UpdateValue(ChangeEventArgs e) { DisplayValue = e.Value.ToString(); } } Свяжите ползунок с только что добавленным обработчиком события Change. Добавьте атрибут @onchange к тегу <input>: @page "/" <div class="container"> <div class="row"> <h1>Experiment with controls</h1> </div> <div class="row mt-2"> <div class="col-sm -6"> Pick a number: </div> <div class="col-sm -6"> <input type="range" @onchange="UpdateValue" /> </div> </div> <div class="row mt-5"> <h2 > Here's the value: <strong>@DisplayValue</strong> </h 2> </div> </div> 3 4 Метод UpdateValue является обработчиком ChangeEvent. Он получает один параметр, который может использовать- ся вашим методом для выпол- нения каких-либо действий с измененными данными. Обработчик события Change обновляет DisplayValue каждый раз, когда он вызывается со значением. При каждом измене- нии DisplayValue зна- чение, отображаемое на странице, тоже будет изменяться. Вы добавили на стр ани цу это значение, кото - рое обновляется при каждом из- менении ползунка. Когда вы используете@onchange для связывания элемента управления с обработчиком события Change, ваша страница будетвызывать обработчик события при каждом изменении эл ем ента упр авл ени я. близкое знакомство с c#
750 Visual Studio для пользователей Mac добавление текстового поля Добавление текстового поля Этотпроектсоздавалсядлятого,чтобы вы могли поэкспериментировать сразнымивидамиэлементов управления. Добавьте элемент текстового поля, чтобы пользователь мог ввести текст в приложении. Введенный им текстбудет воспроизведен в нижней части страницы. Добавьте текстовое поле в разметку HTML-страницы. Добавьте тег <input.../>,практическиидентичныйтому,которыйбылдобавлендляползунка. Существует толькоодноотличие: атрибутуtextприсваиваетсязначение "text"вместо "range". Разметка HTML выглядит так: <div class="container"> <div class="row"> <h1>Experiment with controls</h1> </d iv> <div class="row mt-2 "> <div class="col-sm -6 "> Enter text: </div> <div class="col-sm -6 "> <input type="text" placeholder="Enter text" @onchange="UpdateValue" /> </div> </div> <div class="row mt-2"> <div class="col-sm-6"> Pick a number: </div> Сновазапустите своеприложение— теперь внем присутствует элемент текстового поля.Весь вводимый текст будет отображаться в нижней части страницы. Попробуйте изменить текст, сдвиньте ползунок, а затемсноваизмените текст. Значение внижнейчастиизменяетсякаждый раз, когда вы изменяете состояниеэлемента. 1 Разметка текстового поля. Это элемент управ- ления, который имеет тип "text" и использу- ет такой же тег @onchange, как и ползунок. Также он содержитдополнительный атрибут дляназначения текста заполнителя, так что доввода текста элемент выглядит так: Возможно, после ввода текста вам придется на- жать Enter, чтобы зарегистрировать изменения и запустить обработчик события. Обработчик события обновляет текст, как и прежде. В страни- цу с верх- ним полем, размер к отор ого сос тав - ляет две единицы, добав- ляется еще одна строка.
дальше 4 751 начинаем программировать на C# Добавьте метод — обработчик события, который принимает только числовые значения. А еслитекстовоеполедолжно принимать только числовой ввод отпользователей?Добавьте следующий метод вкод междуфигурнымискобками внижнейчасти страницы Razor: private void UpdateNumericValue(ChangeEventArgs e) { if (int.TryParse(e.Value.ToString(), out int result)) { DisplayValue = e.Value.ToString(); } } Измените текстовое поле для использования нового обработчика события. Измените атрибут@onchange текстового поля, чтобы для него вызывался новый обработчик события: <input type="text" placeholder="Enter text" @onchange="UpdateNumericValue" /> Теперь попробуйтеввести текствтекстовом поле— значениевнижнейчастистраницы обнов- ляетсятолько в томслучае, есливведенный текстявляетсяцелочисленным значением. 2 3 Вы узнаете все об int.TryParse позд- нее в этой кни- ге — пока просто введите код точно в таком виде, в каком он приве- ден здесь. Элементы Button использовались в игре с поиском пар в главе 1. Ниже приведен фрагмент разметки HTML для добавления ряда кнопок на страницу — он очень похож на тот код, кото - рый мы использовали ранее. Ваша задача — завершить этот код, чтобы он добавлял шесть кнопок, а также включить обработчик события в код C#. <div class="row mt-2"> <div class="col-sm -6 ">Pick a number:</div> <div class="col-sm -6 "><input type="range" @onchange="UpdateValue" /></div> </div> <div class="row mt-2 "> <div class="col-sm-6">Click a button:</div> <div class="col-sm-6 btn-group" role="group"> { string valueToDisplay = $"Button #{buttonNumber}"; < butt on typ e="b utt on" cla ss= "btn bt n-s econ dar y" @on clic k=" () => B utt onCl ick (va lueT oDi spl ay)" > @ but ton Numb er </button> } </d iv> </d iv> <div c lass="row mt-5"> <h 2> Here's t he value: <strong>@D isplayValu e</strong> </ h2> </div> Замените этот пря- моугольник строкой кода C#, котор а я размещает на странице шесть кнопок. При щелчке на кнопке вызывается обработчик события с именем ButtonClick. Добавьте этот метод в нижнюю часть страницы — он состо- ит только из одной команды. Установите в методе точку прерывания, воспользуй- тесь отладчиком и разберитесь в том, как она работает. близкое знакомство с c# Упражнение
752 Visual Studio для пользователей Mac другие элементы управления Ниже приведена строка кода, которая заставляет разметку Razor разместить шесть кнопок на странице. Это цикл for, и он работает точно так же, как цикл for, о котором вы узнали в главе 2. <div class="row mt-2"> <div class="col-sm-6">Pick a number:</div> <div class="col-sm-6"><input type="range" @onchange="UpdateValue" /></div> </d iv> <div class="row mt-2"> <div class="col-sm-6">Click a button:</div> <div class="col-sm-6 btn-group" role="group"> @for (var buttonNumber = 1; buttonNumber <= 6; buttonNumber++) { string valueToDisplay = $"Button #{buttonNumber}"; <button type="button" class="btn btn-secondary" @onclick="() => ButtonClick(valueToDisplay)"> @buttonNumber </button> } </div> </d iv> <d iv class=" row mt-5"> <h2> Here 's the val ue: <stron g>@Display Value</str ong> </h2> </d iv> Следующий обработчик события добавляется в код в нижней части страницы. Он присваивает DisplayValue зна- чение, переданное в параметре, при щелчке на кнопке: private void ButtonClick(string displayValue) { DisplayValue = displayValue; } Цикл for, создающий кнопки, работает точно так же, как в игре с поиском пар, — код совпадает практи- чески полностью. Кнопки объединены стилевым оформлением в группу(это делает btn-group) и окра- шиваются в разныецвета (это делает btn-secondary). Упражнение Реше ни е
дальше 4 753 начинаем программировать на C# Добавление селекторов для выбора цвета и даты Селекторы(pickers) — всеголишь ещеодна разновидность элементовввода данных. Селектордатыиме- еттип "date",а селекторцвета имееттип "color" — в остальном разметкаHTML дляэтих элементов управления неотличается. Изменитесвоеприложение идобавьте внегоселекторыдатыицвета. Нижеприведена разметкаHTML— добавьте ее над тегом <div> для отображаемого значения: <div class="row mt-2"> <div class="col-sm -6 ">Pick a date:</div> <div class="col-sm -6"> <input type="date" @onchange="UpdateValue" /> </div> </div> <div class="row mt-2"> <div class="col-sm -6 ">Pick a color:</div> <div class="col-sm -6"> <input type="color" @onchange="UpdateValue" /> </div> </div> <div class="row mt-5"> <h2>Here's the value: @DisplayValue</h2> </div> </div> Селекторы даты и цве- та используют один обработчик события Change, так что вам вообще не придется из- менять код для вывода цвета или даты, вы- бранных пользователем. Выберите значение в селекто- ре цвета, и он вызовет тот же самый обработчик для обновления значения в нижней части страницы. Проект подошел к концу — отличная работа! Вы можете вернуться к главе 2 в самом конце, где некто в кресле размышляет: СТОЛЬКО РАЗНЫХ СПОСОБОВ ВЫБОРА ЧИСЕЛ! У МЕНЯ ПОЯВЛЯЕТСЯ НЕВЕРОЯТНО МНОГО ВАРИАНТОВ ПРИ РАЗРАБОТКЕ ПРИЛОЖЕНИЯ. близкое знакомство с c#
754 Visual Studio для пользователей Mac повторное использование класса Ориентируемся на объекты В середине главы 3 была приведена Windows-версия проекта для выбора карт. Мы воспользуемся Blazor, чтобы построить веб-версию того же приложения. Построение Blazor-версии приложения для выбора карт Вследующемпроекте мыпостроимприложениеBlazorс именемPickACardBlazor. Внемползунокбудет ис- пользоватьсядлявыбораколичестваслучайныхкарт;картыбудут выводитьсяв списке.Вот как этовыглядит: Ползунок исполь- зуется для выбора количества карт. Нажмите кнопку, чтобы выбрать задан- ное количество карт и добавить их в список. Обработчик события этой кнопки вызывает метод класса, который возвращает список карт, после чего включает каждую карту в массив. Мы воспользуемся циклом для преобра- зования массива карт в серию тегов HTML, по аналогии с тем, как это делалось с кноп- ками в предыдущих проектахBlazor. Повторное использование класса CardPicker в приложении Blazor Есливынаписаликласс для однойпрограммы,часто бывает возможно использовать то же поведение в другой программе. Одно из главных преимуществ классов как раз изаключается в том, что ониупро- щаютповторноеиспользование кода.Давайте создадимдляприложениякрасивый новыйинтерфейс, но сохраним прежнееповедение за счетповторного использования классаCardPicker. Создайте новый проект Blazor WebAssembly App с именем PickACardRazor Выполнитеточно такиеже действия, какие были выполнены при создании игры с поиском пар вглаве1: ‘ ОткройтеVisual Studio исоздайте новыйпроект. ‘ ВыберитеBlazorWebAssembly App,как ив предыдущих приложенияхBlazor. ‘ Присвойтеприложению имяPickACardBlazor. VisualStudio создастпроект. 1 Из главы 3 Это Blazor­версия проекта настоль- ного приложения Windows из главы 3.
дальше 4 755 начинаем программировать на C# Добавьте класс CardPicker, созданный для проекта консольного приложения. Щелкнитеправой кнопкой мыши на имени проекта и выберите команду меню Add>>Existing Files...: Перейдитев папку с консольным приложением ищелкните на файлеCardPicker.cs,чтобы доба- вить его в проект. VisualStudio спросит,что вы хотитесделать: скопировать, переместить или создать ссылку нафайл. ПрикажитеVisual Studio скопировать файл. Теперь в вашем проекте должна появитьсякопия файлаCardPicker.csиз консольного приложения. Измените пространство имен для класса CardPicker. Сделайте двойнойщелчок на файлеCardPicker.cs вокнеSolution. В немостается пространство имен из консольного приложения. Измените пространство имен и приведите его в соответ- ствиес именем проекта: Теперь класс CardPicker должен принадлежать пространству именPickACardBlazor: namespace PickACardBlazor { class CardPicker { Поздравляем, выповторно использоваликлассCardPicker!Класс долженпоявиться вокнеSolution, и вы можете использовать его в кодеприложенияBlazor. 2 3 Вы изменяете пространство имен в файле CardPicker.cs , чтобы оно совпа- дало с пространством, использованным при создании файлов в новом проек- те. Это необходимо для использования класса CardPicker в коде нового проекта. Например, открыв файл Program.cs, вы увидите, что он принадлежит к тому же пространству имен. ориентируемся на объекты
756 Visual Studio для пользователей Mac построение макета страницы приложения <div class="container"> <div class="row"> <div class="col-8 "> </div> </div> </div> <div class="col-4"> </div> </div> </div> </div> <div class="row "> <div class="row mt-5 "> <div class="row mt-5 "> Страница состоит из строк и столбцов ВприложенияхBlazorизглав 1и2длясозданиястроки столбцов использовалась разметкаHTML,и новое приложениеделает тожесамое.Наследующейдиаграммепредставленаструктуравашегоприложения: Левый столбец делится на три с тр оки. Все приложение нахо- дится внутри контей- нера, который содержит строку, разделенную на два столбца. Следующий код генерирует список карт в правом столбце. Он использует цикл foreach(вроде того, который использовался в игре с поиском пар)для создания списка из массива с именем pickedCards: <div class="col-4"> <ul class="list-group"> @foreach (var card in pickedCards) { <li class="list-group-item">@card</li> } </ul> </div> Список начинается с тега <ul class="list-group"> и завершает- сятегом </ul>(сокращениеul означает «ненумерованныйсписок» (Unnumbered List)). Каждый элемент списка начинается с тега <li class="list-group-item"> и завершается тегом </li>. <div class="col-4"> <u l> <li>Ace of Diamonds</li> <li>2 of Hearts</li> <li>2 of Clubs</li> <li>6 of Hearts</li> <li>8 of Diamonds</li> <li>4 of Diamonds</li> </ ul> </d iv> Так создается список с раз- меткой HTML.
дальше 4 757 начинаем программировать на C# Ползунок использует связывание данных для обновления переменной Код внижнейчастистраницы начинаетсяс переменнойс именем numberOfCards: @code { int numberOfCards = 5; Дляобновления numberOfCards можно использовать обработчик события, но в Blazor существуетболее эффективный механизм связывания данных, позволяющий настроить элементы ввода для автомати- ческого обновленияданных C#и автоматической вставкизначений из кода C#в страницу. Ниже приведена разметка HTML для заголовка, ползунка и текста с текущим значением: <d iv cl ass=" row"> <h3 >How many cards shou ld I pick? </h3> </ div> <d iv cl ass=" row m t-5"> <in put t ype=" range " cla ss="c ol-10 form -cont rol-r ange" m in="1 " max ="15" @bin d="nu mberO fCard s" /> <di v cla ss="c ol-2" >@num berOf Cards </div > </ div> Присмотритесь повнимательнее к атрибутам тега input. Атрибуты min и max ограничивают ввод зна- чениями от 1 до 15. Атрибут@bind настраивает связывание данных, так что при каждом изменении состояния ползункаBlazor автоматическиобновляетnumberOfCards. За тегом input следуетфрагмент <div class="col-2 ">@numberOfCards</div> — эта разметка добав- ляеттекст (ml-2 означает расширение левого поля).Здесь также используется связываниеданных, но в другом направлении: при каждом обновлении поля numberOfCards Blazor автоматически обновляет текст внутри тега div. Мы предоставили почти все необходимое для добавления разметки HTML и кода в файл Index. razor. Удастся ли вам собрать все воедино, чтобы веб-приложение заработало? Шаг 1: Завершение разметки HTML Первые четыре строки файла Index.razor идентичны первым четырем строкам приложения ExperimentWithControl- Blazor из главы 2.Следующие две строки HTML приведены в верхней части снимка экрана, где мы объясняем, как работают строки и столбцы. Единственная разметка, которую мы еще не приводили, относится к кнопке — вот она: <button type="button" class="btn btn-primary" @onclick="UpdateCards">Pick some cards</button> Шаг 2: Завершение кода Мы предоставили вам начало раздела @code в нижней части страницы, в нем присутствует поле int с именем numberOfCards. • Добавьте поле со строковым массивом: pickedCards: string[] pickedCards = new string[0]; • Добавьте обработчик события UpdateCards, вызываемый нажатием кнопки. Он вызывает CardPicker. PickSomeCards и присваивает результат полю pickedCards. Когда вы введете этот код, IDE может добавить разрыв строки после открывающего и перед закрывающим тегом. ориентируемся на объекты Упражнение
758 Visual Studio для пользователей Mac решен ие упр аж нени я Ниже приведен полный код файла Index.razor . Вы также можете повторить действия из проекта ExperimentWithControlsBlazor, чтобы удалить лишние файлы и обновить меню навигации. @page "/ " <div class="container"> <div class="row"> <div class="col-8"> <div class="row "> <h3>How many cards should I pick?</h3> </div> <div class="row mt-5"> <input type="range" class="col-10 form-control-range" min="1" max="15" @bind="numberOfCards" /> <div class="col-2">@numberOfCards</div> </div> <div class="row mt-5"> <button type="button" class="btn btn-primary" @onclick="UpdateCards"> Pick some cards </button> </div> </div> <div class="col-4"> <ul class="list-group"> @foreach (var card in pickedCards) { <li class="list-group-item">@card</li> } </u l> </div> </d iv> </d iv> @code { int numberOfCards = 5; string[] pickedCards = new string[0]; void UpdateCards() { pickedCards = CardPicker.PickSomeCards(numberOfCards); } } Обработчик события Click, связан- ный с кнопкой, вызывает метод PickSomeCards в классе CardPicker, написанном ранее в этой главе. Когда вы щелкаете на кнопке, ее обработчик события Click с име- нем UpdateCards присваивает массиву pickedCards новый набор случайных карт. Сразу же после изменения в происходящее вмеши- вается механизм связывания дан- ных Blazor, и цикл foreach авто- матически выполняется заново. numberOfCards и pickedCards — особые разновидности переменных, называемые полями. Вы узнаете о них в главе 3. П олз унок и следую- щий за ним текст раз- мещаются в столб- цах своей маленькой строки. Упражнение Реше ни е
дальше 4 759 начинаем программировать на C# ¢ Классы содержат методы, а методы состоят из команд, выполняющих действия. В хорошо спроектированных классах используются содержательные имена мето- дов. ¢ Некоторые методы имеют возвращаемый тип, кото - рый задается в объявлении метода. Метод с объяв- лением, начинающимся с ключевого словаint, возвра- щает значение int. Пример команды, возвращающей значение int: return 37; ¢ Если метод имеет возвращаемый тип, он должен содержать команду return со значением, соответ - ствующим возвращаемому типу. Таким образом, если в объявлении метода указан строковый возвращае- мый тип, этот метод должен содержать командуreturn, которая возвращает строку. ¢ Как только в методе будет выполнена команда return, программа возвращается к команде, из которой был вызван метод. ¢ Не все методы имеют возвращаемый тип. Метод с объявлением, начинающимся с public void, ниче- го не возвращает. При этом для выхода из мето- да void может использоваться команда return: if (finishedEarly) { return; } ¢ Разработчики часто хотят повторно использовать один код в разных программах. Классы помогают рас- ширить возможности повторного использования кода. Проект подошел к концу — отличная работа! Вы можете вернуться к главе 3 и продолжить с раздела с заголовком: «Прототипы Анны выглядят замечательно...» Ваши веб-приложения Blazor используют Bootstrap для формирования макета. Приложениевыглядитнеплохо!Отчастиэтосвязанос тем,чтооно использует Bootstrap—бесплатныйфреймворкс открытымкодом длясозданиявеб-страниц, которыеавтоматическиадаптируютсякизменениюразмеровэкрана ихорошо работают на мобильныхустройствах. Макетсострокамиистолбцами,управляющиймакетом приложения,происходит непосредствен- но из Bootstrap.Вашеприложениеиспользуетатрибут class(неимеющий никакого отношения кклассам C#)для того, чтобы получить доступ ксредствам формирования макетаBootstrap. <div class="container"> <div class="row"> <div class="col-4"> <div class="col-8"> <div class="row"> <div class="row"> <div class="row"> Поэкспериментируйте: попробуйте заменить col-8 и col-4 на col-6, чтобы столбцы имели оди- наковыеразмеры.Что произойдет, есливыбратьчисла,которые всумменедают12? Bootstrap такжепомогаетсостилевым оформлением элементов. Попробуйтеудалить атрибут class изтеговbutton,input,ul илиliисновазапуститеприложение. Онопродолжает работать точно так же, но выглядит иначе — элементы потеряличасть своего оформления. Попробуйтеудалить все атрибуты class — строкии столбцыисчезают, но приложение все ещеработает. За дополнительной информацией о Bootstrap обращайтесь по адресуhttps://getbootstrap.com. Засценой Ширина контейнеров Bootstrap равна 12, так что ширина столбца "col­4" равна половине ширины столбца "col­8", а вместе они занимают полную ширину контейнера. ориентируемся на объекты КЛЮЧЕВЫЕ МОМЕНТЫ
760 Visual Studio для пользователей Mac этот бифштекс не старый... он винтажный Типы и ссылки В конце главы 4 был приведен проект для Windows. Мы построим Blazor-версию этого проекта. Из главы 3 Это Blazor­версия проекта настоль- ного приложения Windows из главы 4 Добро пожаловать в забегаловку эконом-класса «У неторопливого Джо»! «У неторопливогоДжо»подают сэндвичи.У негоесть мясо, горахлеба ибольше приправ, чемвы можете себе представить. Но вот менюу негонет!Сможете ли вы построить программу,котораягенерируетслучайное новоеменю на каждый день?Да,выопределенно можетеэто сделать...припомощи нового приложе- ния Blazor WebAssembly App,массивови пары полезных приемов. Добавьте в проект новый класс MenuItem с набором полей. Взглянитенадиаграммукласса.Он содержитшестьполей: экземплярRandom, три массива для хранения различных частей сэндвича и строковые поля с описаниями и ценой. Поля-массивы используют инициализаторы кол- лекций: выопределяетеэлементы массива,заключая ихвфигурныескобки. class MenuItem { public Random Randomizer = new Random(); public string[] Proteins = { "Roast beef", "Salami", "Turkey", "Ham", "Pastrami", "Tofu" }; public string[] Condiments = { "yellow mustard", "brown mustard", "honey mustard", "mayo", "relish", "french dressing" }; public string[] Breads = { "rye", "white", "wheat", "pumpernickel", "a roll" }; public string Description = ""; public string Price; } Добавьте метод Generate в класс MenuItem. Этотметодиспользуетуже хорошознакомый вамметодRandom.Nextдлявыбора случайных элементовиз массивов вполях Proteins, Condiments иBreads и ихконкатенациив строку: public void Generate() { string randomProtein = Proteins[Randomizer.Next(Proteins.Length)]; string randomCondiment = Condiments[Randomizer.Next(Condiments.Length)]; string randomBread = Breads[Randomizer.Next(Breads.Length)]; Description = randomProtein + " with " + randomCondiment + " on " + randomBread; decimal bucks = Randomizer.Next(2, 5); decimal cents = Randomizer.Next(1, 98); decimal price = bucks + (cents * .01M); Price = price.ToString("c"); } 1 2 MenuItem Randomizer Proteins Condiments Breads Description Price Generate Метод Generate вычисляет случайную цену в диапазоне от2.01до 5.97 пре- образованием двух случайных int в decimal. Внимательно присмотритесь к последней строке — она возвращает price.ToString("c") . Параметр метода ToString определяет формат. В данном случае формат "c" приказывает ToString отформатировать значение с локальной денежной единицей: в США будет вы- водиться знак $, в Великобритании — ₤, вЕвропе—€,ит.д. Сделайте это!
дальше 4 761 начинаем программировать на C# Добавьте раздел @code в конец файла Index.razor . Он добавляет пять объектов MenuItem в поле menuItems, а также иницализирует поле guacamolePrice. Шаг 1: Добавьте метод OnInitialized Выуже использовали метод OnInitialized для случайной перестановки животных в игре с поиском пар. Добавьте следующую строку кода: protected override void OnInitialized() Шаг 2: Замените тело метода OnInitialized кодом создания объектов MenuItem IDE автоматически заполняеттелометода(base.OnInitialized();), как это было сделано при создании игры с поиском пар. Удалите эту команду. Замените ее кодом, инициализирующим поля menuItems и guacamolePrice. • Напишите цикл for, который добавляет пять объектов MenuItem в поле с массивом manuItems и вызывает их методыGenerate. • Присвойте полюBreads новый массив строк: new string[] { "plain bagel", "onion bagel", "pumpernickel bagel", "everything bagel" } • Создайте новый экземпляр MenuItem, вызовите его метод Generate и используйте поле Price для задания guacamolePrice. container row col-3 col-9 row col-3 col-9 row col-3 col-9 row col-3 col-9 row col-3 col-9 col-6 Нижняя строка содержит один столбец, ширина которого со- ставляет половину от шири- ны контейнера. Этой строке назначен класс justify­content­ center, который обеспечивает выравнивание нижней строки по центру страницы. типы и ссылки Упражнение Добавьте макет страницы в файл Index.razor. Меню страницы состоит из серии строкBootstrap, по одной для каждого пункта меню. Каждаястрока содержитдвастолбца: col-9 с описанием элемента меню и col-3с ценой. Внизудобавлена последняя строка со столбцом col-6,выровненным по центру. @page "/" <div class="container"> @foreach (MenuItem menuItem in menuItems) { <div class="row"> <div class="col-9"> @menuItem.Description </div> <div class="col-3"> @menuItem.Price </div> </div> } <div class="row justify-content-center"> <div class="col-6"> <strong>Add guacamole for @guacamolePrice</strong> </div> </div> </div> @code { MenuItem[] menuItems = new MenuItem[5]; string guacamolePrice; } 3
762 Visual Studio для пользователей Mac случайные сэндвичи Нижеприведен полный код файла Index.razor . Все до string guacamolePrice; соответствует коду, который я вам предоставил, а вам было предложено заполнить остаток блока @code. @page "/ " <div class="container"> @foreach (MenuItem menuItem in menuItems) { <div class="row"> <div class="col-9"> @menuItem.Description </div> <div class="col-3"> @menuItem.Price </div> </div> } <div class="row justify-content-center"> <div class="col-6"> <strong>Add guacamole for @guacamolePrice</strong> </div> </d iv> </d iv> @code { MenuItem[] menuItems = new MenuItem[5]; string guacamolePrice; protected override void OnInitialized() { for(inti=0;i<5;i++) { menuItems[i] = new MenuItem(); if(i>=3) { menuItems[i].Breads = new string[] { "plain bagel", "onion bagel", "pumpernickel bagel", "everything bagel" }; } menuItems[i].Generate(); } MenuItem guacamoleMenuItem = new MenuItem(); guacamoleMenuItem.Generate(); guacamolePrice = guacamoleMenuItem.Price; } } Страница использует эти два поля для связывания данных. Поле menuItems используется для генерирования пяти строк, тогда как поле guacamolePrice содержит цену для пункта меню в нижней части таблицы. Значение присваивается напрямую элементу массива. Также можно было создать отдельную перемен- ную для создания нового экземпляра MenuItem и присвоить ее элементу массива в конце цикла for. Не забудьте вызвать метод Generate; в противном случае поля MenuItem будут пустыми, а стра- ница останется в основном пустой. Упражнение Реше ни е
дальше 4 763 типы и ссылки Проект подошел к концу! Вы можете вернуться к разделу «Ключевые моменты» в самом конце главы 4. Я обедаю только «У НеторопливогоДжо»! Под увеличительным стеклом Breads[Randomi zer.N ext(Breads.L ength)] Breads — строковый массив с пятью элементами, пронумерованными от 0до 4. Таким образом, Breads[0] содержит "rye", а Breads[3] содержит "a roll". Метод Randomizer.Next(7) получает случайное число, меньшее 7. Breads.Length возвращает количество элементов в массиве Breads. Следовательно, Randomizer.Next(Breads.Length) возвращает случайное число, большее или равное нулю, но меньшее количества элементов в массиве Breads. Если ваш компьютер достаточно производителен, возможно, ваша программа не столкнется с этой проблемой. Но если запустить ее на более медленном компьютере, вы наверняка увидите ее. Запустите программу и просмотрите сгенерированное меню. Э-э -э ... Что-то не так. Всецены в новом меню одина- ковы,ипунктыменю выглядят странно: первыетри позициисовпадают,потомследующиедве,иначина- ютсяони одинаково. Что происходит? Оказывается, класс .NET Random на самом деле явля- етсягенератором псевдослучайных чисел;этоозна- чает,чтоонпоматематическойформулегенерирует последовательность чисел, удовлетворяющихнекото- рымстатистическимкритериямслучайности.Такие числа достаточно хороши для любых приложений, которыестроим мыс вами(тольконе используйте их всистемахбезопасности,зависящих от действитель- но случайныхчисел!).Вотпочему метод называется Next— он выдаетследующеечисло в последователь- ности. Формула начинается со специального значе- ния, которое называется «затравкой», — это значение используется для вычисленияследующего значения в серии. Когдавысоздаете новыйэкземплярRandom, системныечасы используютсядляполучения«затравки» формулы,но выможетезадать собствен- ное значение. Попробуйте ввести в интерактивном окнеC# вызов new Random(12345).Next(); несколько раз. Тем самым выприказываетесоздать новыйэкземплярRandom содним начальным значением (12345),поэтому методNextбудеткаждыйраздавать одно и то же «случайное» число. Когда вы видите, что несколько разных экземпляровRandom дают одно и то же значение, это объясняется тем, что они инициализировались практически водин моментвремени, показания системных часов не изменились, поэтому все они использовали одно начальное значение. Как решитьпроблему?Используйте один экземплярRandom иобъявите полеRandomizerстатическим, чтобы всекомандыMenuItem совместно использовалиодин экземпляр Random: public static Random Randomizer = new Random(); Сновазапуститепрограмму — на этотраз меню станетслучайным. 4 Почему пунк­ ты меню и цены оста- ются одина- ковы м и?
764 Visual Studio для пользователей Mac продолжение в интернете За описаниями проектов для глав 5 и 6 обращайтесь на страницу GitHub Возможно, вы заметили, что проекты глав5 и 6 получились более длинными и сложными, чем проекты предшествующихглав.Мыхотимпредоставить наилучшее возможное качество обучения, но для этого потребуетсябольше страниц, чем мы могли бы выделить в этом приложении!По этой причине проекты глав5 и6 «Visual Studio для пользователейMac» доступны для загрузки в форматеPDF на странице GitHub:https://gitgub.com/head-first-csharp/fourth-edition. Превосходно! Но я тут задумался... Как вы считаете, можно ли построить более наглядное приложение для вычисления повреждений? Да! Мы можем построить приложение Blazor, использующее тот же класс. В главе5 мы создали для Оуэна консольное приложение для вы- числения поврежденийвролевойигре.Теперь мыможем повторно использовать тотже класс для проекта ввеб-приложенииBlazor. Система управления ульем Проектизглавы6былсерьезнымбизнес-приложением. Пчелиной матке нужна ваша помощь! Улей вышел из-под контроля, иейнужна программа, котораяпоможетуправ- лять предприятием по производству меда. В улье полно рабочихпчел, массаневыполненнойработы,но каким-то образомруководительница забыла, какаяпчелачем зани- мается,и хватитлиу неепчелиных силдлявыполнения всех незавершенных задач. Вам предложено построить систему управления ульем, которая будет заниматься отслеживанием рабочих ипорученныхимзаданий. Все остальные проекты настольных приложений Windows и проекты Blazor, следующие после главы 6, доступны для загрузки в формате PDF. Поищите их на странице GitHub!
дальше 4 765 Приложение II Ката программирования У вас уже есть опытработы на другомязыкепрограммирования?Возможно, в таком случае ката программирования станутэффективной,результативной и приятной альтернативой для чтения книги «от коркидо корки». «Ката» — японское слово, котороеозначает «форма» или «путь». Оноиспользуетсяво многихбоевых искусствахдляописания метода тренировки,основанногона многократнойотработкесерийдвиже- ний или приемов. Многиеразработчики применяютэту концепцию для отработки своих навыков программирования посредствомнаписанияконкретныхпрограмм,чащеболееодногораза. Многие опытныеразработчики,желающиеосвоитьC#, применяют катапрограммирования как альтернативный путьизучения этих глав. Этотметодработаеттак: • Начиная новую главу,пролистайте илибегло просмотрите ее,пока ненайдетепервыйэлемент ката программирования(см. инструкции ниже).Прочитайтеближайшую врезку «Ключевые моменты», чтобыувидеть,какойматериалрассматривался вэтомразделе. • В описаниях ката программирования приводятся инструкции по выполнению конкретного упражнения — обычно проектиз области программирования. Попробуйте выполнить проект, возвращаясь к предшествующим разделам (особенно врезкам «Ключевые моменты») за допол- нительной информацией. • Есливызайдетев тупик при выполненииупражнения, значит,вы столкнулись с той областью, в которойC#сильно отличаетсяот уже известных вам языков. Вернитесь кпредыдущему элементу ката программирования(или кначалуглавыдляпервого) и начните читатькнигу последовательно до точки,вкоторойвы застряли. • В лабораторных работахUnity вы тренируетесь внаписаниикода C# на примереразработки 3D-игр набазеUnity.Этилабораторныеработы необязательны дляметода катапрограммирования, но мы настоятельно рекомендуем выполнять их. Крометого, это очень увлекательный и по- настоящему интересный способ отточить новообретенные навыки C#. Ката — в боевых искусствах или в программировании — предназначены для многократного повторения. Таким образом,если вы действительно хотитезакрепитьC#у себя в голове, после за- вершенияглавы вернитесь кней,найдитеразделы ската программирования ивыполнитеихснова. Вглаве1ката программирования отсутствуют. Даже опытномуразработчикустоитполностью про- читать этуглавуивыполнить все проектыиупражнения, даже те, которые выполняются«на бумаге». Вглавепредставлены некоторыефундаментальные идеи, которыебудутразвиваться в остальных главах. Крометого, еслиувасестьопытработынадругомязыкепрограммированияивы собираетесь выбратьпуть ката программирования, посмотрите видеоролик ПатрисииАас(Patricia Aas), по- священный изучению C# как второго (или пятнадцатого) языка:https://bit.ly/cs_second_language. Этонеобходимо для путиката программирования. Вы обладаете серь езным опытом работы на другом языке программирования и хотите изучить C# для работы или собственного удовольствия? Поищите разделы ката программирования, на- чиная с главы 2: они формируют альтернативный путь изучения, идеально подходящий для более опытных (или нетерпеливых!) разработчиков. Ознакомьтесь с этим роликом, чтобы понять, подойдет ли этот путь лично вам. Ката программирования для опытных и/или нетерпеливых
766 глава 4 Глава 2: Знакомство с C# В главе 2 наши читатели знакомятся с основными концепциями C#: распределением кода по про- странствам имен, классами, методами и командами, атакже некоторымибазовыми синтаксическими конструкциями. Глава завершается проектом построения простого пользовательского интерфейса для ввода: длячитателей,работающихна платформеWindows, это настольное приложение WPF;для читателей macOS — веб-приложение Blazor (см. приложение «Visual Studioдля пользователей Mac»). Та- кие проекты включаются в большинство глав длятого, чтобы продемонстрировать читателю другие подходык решению аналогичныхзадач, чтоможет сильно пригодитьсяпри изучении нового языка. Дляначалапросмотрите всю главуи прочитайте всеразделы«Ключевые моменты». Просмотрите все примеры кода.Что-то показалось знакомым? Втаком случае выготовыприступить кнекоторымката. Ката No1:Найдите элемент Сделайте это! в разделе с заголовком «Генерирование нового методадля работы с перемен- ными». Он станет вашей отправной точкой.Добавьте код в этот раздели в следующийраздел «Добавление кодас использованием операторов» в консольное приложение, созданное в главе 1.Воспользуйтесь отладчиком VisualStudioдля пошагового выполнениякода — в следующем разделе показано, какэтоделается. Ката No2:В следующих разделахприводятся примеры команд if ициклов,которые вы должны до- бавить всвое приложение и отладить. Всевыглядит логично?В таком случае вы готовы взяться за проектWPF вконце главы или за про- ектBlazorвприложении«VisualStudioдля пользователейMac». Есливыувереннообращаетесь сVisual Studio иможете сориентироваться вIDEдлясоздания,выполненияилиотладки консольного при- ложения .NET Core, значит,вы готовы для... Глава 3: Объекты Вэтойглаве представлены основные концепцииклассов, объектови экземпляров. В первой поло- вине мы работаем со статическими методамиилиполями(т. е. принадлежащимисамомутипу,а не его конкретному экземпляру); во второйполовинебудутсоздаваться экземпляры классов. Есливы выполняете— и понимаете — эти ката,можетеспокойно переходить кследующейглаве. Ката No1:Первый проект этой главы — простая программа, генерирующая случайныеигральные карты. Найдите первый элемент Сделайте это!(рядом с заголовком «Создание консольного приложения PickRandomCards»). Этовашаотправная точка. Создайтекласс и используйтеего впростой программе. Ката No2:Проработайтематериал следующих разделови завершитекласс CardPicker. Этоткласс будет повторно использованв настольном приложенииWPF илив веб-приложенииBlazor. Ката No3:Пролистайте илибегло просмотритеглаву до раздела «Возьми врукукарандаш», в кото- ромсоздаютсяобъектыClown. Введитекод,добавьтекомментарии дляответов,а затем выполните впошаговом режиме. КатаNo4:Ближек концуглавынайдите заголовок «Классы,парнииденьги».Сразуже после него приведенкласс с именемGuy, за которымследуетупражнение.Выполните его, чтобыпостроитьпростое приложение. Ката No5:Выполнитеследующееупражнение, в котором класс Guy повторно используется в про- стойигре со ставками. Предложите какминимум один способулучшенияигры:предоставьте игроку возможность выбирать разные шансы и ставки,включите поддержку нескольких игрокови т. д. В процессе работы над программами обращайте особое внимание на места, в которых вы могли бызадать себеследующие вопросы(ответы на них приводятся в последующих главах, но если вы уделитеим вниманиесейчас,это поможетвам быстрееосвоитьC#): • Нет ли вC# какой-либоразновидностикомандыswitch? • Разве некоторыеразработчики не считают наличие нескольких команд return в методе или функцииплохой практикой? • Почемумыиспользуемreturn длявыхода изцикла?Нет ливC#болееудобного способа выхода из цикла без возвратауправленияиз метода?
дальше 4 767 Глава 4: Типы и ссылки Этаглава посвящена типам и ссылкам в C#. Прочитайте несколько первыхразделов и убедитесь втом, что достаточно знаетео разных типах: скажем,у чисел с плавающей точкой есть некоторые странности, о которых необходимо знать. Просмотрите диаграммы и проверьте свое понимание того,какработают ссылки,потомучто эти особенности ихповедениябудут использоваться вкниге. Всеусвоили?Хорошо,переходитектрем ката. Если высправитесь с нимибез особых затруднений, можно смело переходить к следующейглаве. КатаNo1:Первыйпроект этойглавы —программа для расчетахарактеристикперсонажавролевой игре. Начните с заголовка «Поможем Оуэну в экспериментах с характеристиками». Вы должны найти и исправить каксинтаксическую ошибку,так иошибкув логике, незаглядывая вответ. Ката No2:Займитесь проектом вупражнении, которое начинается со слов: «Создайтепрограмму с классом Elephant». Когда упражнениебудетвыполнено, просмотритевесь материал вплоть до за- головка «Объекты используютссылкидлявзаимодействия другс другом». Ката No 3: Выполните проект под заголовком «Добро пожаловать в забегаловкуэконом-класса “У неторопливого Джо”!» Нескольковопросов, которые стоит принять во внимание (ответы на них даютсяпозднее вкниге): • Различаются ли вC# типы значений(такие, как double и int) и ссылочные типы(такие, как Elephant, MenuItem и Random)? • Можно ли как-то управлять временем уничтожения сборщиком мусора тех объектов, на ко- торые неосталось ни одной ссылки? • КаквIDEотслеживать иразличать конкретныеэкземпляры? Размышлениянадэтими вопросамипомогутвамбыстрееосвоить C#. Глава 5: Инкапсуляция Вэтойглаверечьпойдетоб инкапсуляции— вC# подэтим термином подразумеваетсяограничение доступа к некоторым компонентам классов, чтобы предотвратить их некорректноеиспользование внешними объектами. Если вы справитесь с последним ката (где требуется исправить ошибку, вне- сенную в первом),не подсматриваяврешение,можете смело переходить кследующейглаве. КатаNo1:Первыйпроектэтойглавы — программа длярасчета поврежденийвролевойигре. Онна- чинаетсяот начала главы иследует до врезки«Великийсыщик». Убедитесь втом,чтовы понимаете, почему программа неработает. Бегло просмотритеупражнение вконцеглавы. Если вы справитесь с ним,не подглядывая вответ, значит,вы уже готовы перейти кследующейглаве. КатаNo 2: Выполните проект изупражнения, которое начинается со слов: «Чтобы немного потрениро- ваться в использовании ключевого слова private, мы создадим маленькую игру “Больше-меньше”». Убедитесь втом, что вы понимаете, как в нем используются ключевые слова const, public и private. Не за- будьте о дополнительной задаче. КатаNo3: Реализуйте маленький проект из врезки «Будьте осторожны!», которая начинается со слов: «Инкапсуляция — не то же самое, что безопасность. Приватные поля не за- щищены». Прочитайте следующие разделы: «Свойстваупрощают инкапсуляцию», «Автоматическиреализуемые свойстваупрощают ваш код» и «Использование приватного set-методадля создания свойств, доступных толькодля чтения». Ката No4: Выполните последнее упражнение в этой главе, в котором инкапсуляция применяется для исправленияклассаSwordDamage.
Глава 6: Наследование Темой этой главы является наследование; в C# это означает создание классов, которые повторно используют, расширяют и изменяют поведение другихклассов. Если вы пошли по пути ката про- граммирования, весьма вероятно,что вы уже знаете какой-нибудь язык с наследованием. Эти ката дают представление о синтаксисе наследования в C#. КатаNo1:Первыйпроект этойглавырасширяеткалькуляторповрежденийизглавы4.Наследование внем неиспользуется —онвсего лишь показываетначинающимпрограммистам, вчем ценность на- следования. Такжевнем представлен синтаксис команды C#switch.Вероятно, вы легкосправитесь с этим упражнением. Преждечем выполнять остальныеката,просмотритеследующиеразделы:«В любомместе,гдеможетисполь- зоваться базовый класс, вместо него можно использовать один из субклассов» и «Некоторые компоненты реализуются тольков субклассах». Затем просмотрите раздел «Субкласс может скрывать методы базового класса» вместе со всеми подразделами. Ката No2: Вернитесь и выполнитеупражнение,которое начинается со слов: «Немного потренируемся в расширениибазовыхклассов». Посленего сбазовым синтаксисом наследованиявC# проблембыть не до лж но. Ката No 3: Выполните проект из раздела «Если базовый класс содержит конструктор,ваш субкласс должен его вызвать». КатаNo 4:Выполнитеупражнениепослераздела «Порадоделать приложениедляОуэна». Упражнениесостоит из двух частей: в первой, «бумажной», вам будет предложено заполнить диаграмму классов, а во второй — написать коддля ее реализации. Есливызавершили первые четыре ката без особых трудностей, можно двигаться дальше. Тем не менее мы настоятельно рекомендуем построить систему управления ульем в конце главы. На самом деле это довольно занятный проект, из которого можно извлечьряд полезных уроков об игровойдинамике, апреобразованиепошаговойигрывигрувреальном временивсегонесколькими строками кода — в самом конце главы. Если у вас возникнут затруднения, будет наиболее эффективно переключиться на последовательное чтение главы от последнего успешно завершенного ката. Поздравляем тех, кто выбрал короткий путь по книге. Если вы добрались до главы 6, следуя по пути «Ката программирования», значит, вы уже полностью готовы к главе 7.