В прошлом уроке мы увидели предел процедурного решения. Имя и запас сил каждого героя хранились в отдельных переменных, действия находились в стороне от данных, а функция заклинания технически могла получить сведения о воине. Код работал, но правила нашего игрового мира существовали только в голове разработчика.
Мы уже наметили другое устройство: данные конкретного героя и подходящие ему действия объединяются в объекте, а их общий шаблон задаёт класс. Теперь перенесём эту модель в C# — сначала разберём общую форму класса, а затем постепенно соберём Mage и создадим по этому описанию конкретных магов.
От идеи к объявлению класса
Объявление класса в общем виде выглядит так:
public class ClassName
{
// Данные и действия будущих объектов
}
Ключевое слово class сообщает C#, что мы объявляем новый класс, а следующее за ним имя обозначает новый тип, который появится в программе. Фигурные скобки образуют тело класса: внутри них мы будем описывать данные и действия будущих объектов.
Слово public открывает класс для использования в других частях программы. Благодаря этому позднее мы сможем обратиться к нашему классу из Program.cs и создать объект. Подробнее управлением доступом мы займёмся в следующих уроках, а пока нам достаточно понимать, зачем public стоит в объявлении.
Применим общий шаблон к нашей задаче:
public class Mage
{
}
Теперь в программе объявлен класс с именем Mage. По соглашению имена классов в C# начинаются с заглавной буквы, поэтому мы пишем именно Mage, а не mage.
Пустые фигурные скобки не означают ошибку. Мы уже создали новый тип, но пока не указали, что должен хранить и уметь каждый маг. При этом ни Альберта, ни Мерлина в программе ещё нет: объявление класса подготавливает описание, а не создаёт конкретного героя.
Из прошлой задачи мы знаем, что каждому магу нужны имя и запас маны. Добавим эти данные внутрь класса:
public class Mage
{
public string Name;
public int Mana;
}
Строки public string Name; и public int Mana; похожи на знакомые объявления переменных: сначала указан тип, а затем имя. Поле Name будет хранить текст, поэтому получило тип string, а целочисленный запас маны находится в Mana типа int.
Разница в расположении. Эти переменные объявлены не внутри функции, а непосредственно в теле класса, поэтому называются полями.
Перед обоими полями снова стоит public. Позже мы будем обращаться к Name и Mana из Program.cs, и такой модификатор разрешает это сделать. Публичные элементы класса по принятому в C# соглашению именуются с заглавной буквы, поэтому мы используем имена Name и Mana.
Теперь класс уже описывает данные мага, но пока ничего не говорит о его поведении. Чтобы прежняя функция заклинания перестала существовать отдельно от героя, её тоже нужно перенести внутрь Mage.
В процедурной версии функция CastSpell получала сведения о герое извне. Теперь заклинание должно выполняться самим магом и работать с его собственными полями. Добавим это действие в класс:
public class Mage
{
public string Name;
public int Mana;
public void CastSpell()
{
if (Mana >= 5)
{
Mana -= 5;
Console.WriteLine($"{Name} произносит заклинание! Осталось маны: {Mana}");
}
else
{
Console.WriteLine($"{Name}: недостаточно маны!");
}
}
}
Функция, объявленная внутри класса, называется методом.
Начало public void CastSpell() состоит из уже знакомых частей. public разрешает вызывать метод из кода за пределами Mage, а void показывает, что метод не возвращает значение. В круглых скобках нет параметров, потому что имя и запас маны больше не нужно передавать извне.
Когда мы вызовем CastSpell у конкретного объекта, записи Name и Mana внутри метода будут означать поля именно этого мага. Метод проверит его запас маны, при успешном заклинании уменьшит его на пять и выведет имя того героя, который выполняет действие. Так данные и работающая с ними логика оказались внутри одного класса.
Создание и независимость объектов
Описание мага готово, но пока у нас есть только шаблон. Чтобы в программе появился конкретный герой, нужно создать объект класса Mage. Вернёмся в Program.cs и напишем:
Mage albert = new Mage();
В левой части Mage обозначает тип переменной. Раньше на этом месте мы использовали встроенные типы вроде int и string, а теперь обращаемся к собственному типу, который только что объявили. Имя albert понадобится программе, чтобы позднее обращаться к созданному магу.
Правая часть new Mage() создаёт новый объект по описанию из класса Mage. Ключевое слово new указывает, что нам нужен новый объект, а Mage() определяет его класс. Круглые скобки здесь связаны с конструкторами — их назначение мы подробно разберём в следующем уроке.
Объект уже существует, но мы ещё не указали, что это Альберт и что у него двадцать единиц маны. Заполним его поля:
albert.Name = "Альберт";
albert.Mana = 20;
В первой строке через переменную albert мы обращаемся к созданному объекту, после точки выбираем его поле Name и записываем туда строку "Альберт". Вторая инструкция тем же способом помещает число 20 в поле Mana этого объекта.
Здесь становится виден практический смысл public. Поля объявлены внутри класса Mage, но код из Program.cs может обратиться к ним через объект и задать значения.
Теперь у Альберта есть данные, поэтому можно вызвать его действие:
albert.CastSpell();
Через точку программа выбирает метод CastSpell у объекта albert. Имя и запас маны передавать в круглых скобках не приходится: метод сам работает с полями того объекта, у которого был вызван.
В начале вызова значение Mana равно 20, поэтому условие выполняется. Метод вычитает пять, сохраняет в поле новое значение 15 и выводит:
Альберт произносит заклинание! Осталось маны: 15
Запись albert.CastSpell() хорошо отражает устройство программы: мы видим не отдельную функцию и набор переданных ей значений, а конкретного героя, который выполняет своё действие.
Класс описывает не одного заранее заданного персонажа, а всех магов такого типа. Используем тот же Mage, чтобы создать Мерлина:
Mage albert = new Mage();
albert.Name = "Альберт";
albert.Mana = 20;
Mage merlin = new Mage();
merlin.Name = "Мерлин";
merlin.Mana = 15;
albert.CastSpell();
merlin.CastSpell();
Обе инструкции new Mage() опираются на одно описание класса, но создают два отдельных объекта. У Альберта появляются собственные Name и Mana, а у Мерлина — свои. После вызова заклинаний у Альберта останется 15 единиц маны, а у Мерлина — 10.
Изменение поля одного объекта не затрагивает другой объект. Именно поэтому нам больше не нужны связанные только цифрами переменные вроде mage1Name, mage1Mana, mage2Name и mage2Mana: данные каждого героя собраны внутри него самого, а один метод CastSpell применяется к тому магу, у которого мы его вызываем.
Отдельный класс для воина
С магами мы получили нужное устройство: их данные и заклинание объединены классом Mage. Однако у воина другой запас сил и другое действие, поэтому для него подготовим собственное описание:
public class Warrior
{
public string Name;
public int Stamina;
public void Attack()
{
if (Stamina >= 5)
{
Stamina -= 5;
Console.WriteLine($"{Name} наносит удар! Осталось выносливости: {Stamina}");
}
else
{
Console.WriteLine($"{Name}: недостаточно выносливости!");
}
}
}
Структура класса уже знакома. Поля Name и Stamina хранят данные каждого воина, а метод Attack проверяет и изменяет выносливость того объекта, у которого был вызван.
Создадим Эдварда и выполним атаку:
Warrior edward = new Warrior();
edward.Name = "Эдвард";
edward.Stamina = 25;
edward.Attack();
Теперь различие между ролями существует не только в договорённостях разработчиков, но и в типах программы. У объектов Mage есть метод CastSpell, а у объектов Warrior — метод Attack. Если мы попытаемся вернуть ошибку из процедурной версии, C# остановит нас:
edward.CastSpell(); // ошибка: у Warrior нет метода CastSpell
Компилятор видит, что edward имеет тип Warrior, проверяет описание этого класса и не находит в нём метода CastSpell. Ошибка обнаруживается ещё до запуска программы, тогда как правильный вызов edward.Attack() остаётся доступен. Так классы не только собирают код по смыслу, но и выражают правила задачи достаточно точно, чтобы часть неверных действий нельзя было записать случайно.
Строк в программе стало немного больше. Мы добавили отдельные классы, описали поля и методы, а при создании персонажей пока по-прежнему заполняем каждое поле отдельной инструкцией. Выигрыш на этом этапе заключается не в краткости, а в устройстве кода.
Имя и запас сил героя теперь принадлежат одному объекту, а подходящее действие находится в классе рядом с этими данными. По записям albert.CastSpell() и edward.Attack() сразу видно, кто выполняет действие, а компилятор не разрешает вызвать у воина метод, предназначенный для мага.
Общее описание тоже больше не приходится повторять для каждого персонажа. Если всем магам понадобится новое поле, мы добавим его в Mage. Если изменятся правила обычного заклинания, достаточно исправить метод CastSpell, которым пользуются объекты этого класса.
Пока поля объявлены как public, поэтому код из Program.cs может напрямую записать в Mana любое целое число, в том числе неподходящее для нашей игры. Сейчас это помогает сосредоточиться на создании классов и объектов; безопасным управлением данными мы займёмся позже.
Раньше нас беспокоили разрозненные переменные, а теперь стало заметно другое неудобство. Сначала мы создаём объект через new Mage(), затем отдельно задаём имя и только после этого заполняем ману. С двумя полями такой код терпим, но при расширении класса строк станет больше, а одно из присваиваний легко забыть. С этой проблемы мы начнём следующий урок и познакомимся с конструктором.
Теперь вы умеете переносить объектную модель в код: объявлять собственный класс, добавлять в него данные и действия, создавать по этому описанию несколько объектов и обращаться к каждому из них через отдельную переменную.
Простой класс выглядит так:
public class Mage
{
public string Name;
public int Mana;
public void CastSpell()
{
// Действие мага
}
}
- Класс задаёт общее описание объектов одного типа.
- Поле — это переменная внутри класса, в которой объект хранит данные.
- Метод — это функция внутри класса, описывающая действие объекта.
publicразрешает коду из других частей программы использовать класс или обращаться к объявленному с этим словом полю и методу.
Конкретный объект создаётся с помощью new:
Mage albert = new Mage();
Здесь Mage — тип переменной, albert — её имя, а new Mage() создаёт новый объект класса Mage. Точное назначение круглых скобок мы разберём вместе с конструкторами.
Для обращения к доступным полям и методам используется оператор точки:
albert.Name = "Альберт";
albert.Mana = 20;
albert.CastSpell();
Каждый отдельный вызов new Mage() создаёт новый объект со своими значениями полей. Поэтому Альберт и Мерлин создаются по одному классу, но расходуют собственные запасы маны независимо друг от друга.
Разные классы позволяют выразить разные правила. У Mage есть CastSpell, а у Warrior — Attack, поэтому вызов edward.CastSpell() компилятор отклонит ещё до запуска программы.
Сейчас после создания объекта мы вручную присваиваем значение каждому полю. В следующем уроке конструктор поможет задавать начальные данные непосредственно при создании объекта.
Класс задаёт общие данные и действия объектов одного типа; поля хранят состояние каждого объекта, а методы описывают его поведение. Теперь разберём несколько коротких ситуаций, применим этот механизм в новой задаче и в конце проверим понимание в разговоре с AI.
Проверяем смысл: класс, поле и метод
Начнём с трёх ситуаций, в которых похожие формулировки приводят к разному пониманию кода.
Какое объяснение точнее всего описывает класс, поле и метод?
Какой смысл у записи public class Robot?
Какое правило важно сохранить при применении темы урока?
Собираем понятия в одну картину
Определения по отдельности уже знакомы. Теперь свяжите саму идею, её запись в C# и ограничение, о котором важно помнить.
Соотнесите элементы слева с подходящими объяснениями.
Применяем: класс, поле и метод
Теперь перенесём идею урока в новую небольшую ситуацию. Решение должно оставаться коротким, но по коду должно быть видно, зачем выбран именно этот механизм. Опишите класс Robot с полями Name и Energy и методом Work, который выводит имя робота. Создайте двух роботов, задайте им разные значения и вызовите метод у каждого.
Подсказка
Подсказка: Начните с главной записи урока: `public class Robot`. Затем проверьте, что код действительно показывает её смысл.
Объясняем: класс, поле и метод
Практическое решение готово. Осталось проверить, сможете ли вы объяснить его другому разработчику: что делает механизм, как читается запись и где проходит его граница.
Ответьте на четыре вопроса своими словами. AI может задать один уточняющий вопрос, если в ответе не хватает важной части.
Ответьте на вопросы собеседника и получите итоговую оценку по теме.
Проходной результат: 70%. Попыток: 3.
Начать собеседование