В прошлом уроке имя и мана мага лежали в отдельных переменных — albertName и albertMana. Мы помнили, что они относятся к одному персонажу, но при вызове функции ничто не мешало передать имя другого героя. Данные подходили по типам, а заклинание в итоге произносил не тот маг.
Сегодня соберём данные и действие вместе. Опишем класс Mage, а затем создадим двух магов — Альберта и Мерлина. У каждого будут свои имя и мана, а заклинание станет методом, который работает с данными своего героя.
Описываем мага
Примеры можно собрать в одном файле Program.cs. Сначала сохраните код прошлого урока отдельно, чтобы к нему можно было вернуться. В новом варианте файла команды создания героев и вызовы их действий будут находиться сверху, а объявления классов — ниже всех этих команд. C# позволяет обращаться к классу, объявленному дальше в файле, но обычные команды после объявления класса размещать нельзя.
Мы будем постепенно дополнять один класс Mage: очередной вариант заменяет предыдущий, а не добавляется рядом с ним. Иначе в программе окажется несколько классов с одинаковым именем.
Объявление класса в общем виде выглядит так:
public class ClassName
{
// Данные и действия будущих объектов
}
Ключевое слово class сообщает C#, что мы объявляем новый класс, а следующее за ним имя обозначает новый тип, который появится в программе. Фигурные скобки образуют тело класса: внутри них мы будем описывать данные и действия будущих объектов.
Слово public перед классом делает этот тип доступным и из других проектов, которые подключат наш код. В пределах текущего проекта такой класс можно использовать и без 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. Если не указать модификатор у этих полей, обращаться к ним можно будет только внутри Mage. Публичные элементы класса по принятому в 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() определяет его класс. Круглые скобки здесь связаны с конструкторами — их назначение мы подробно разберём в следующем уроке.
Объект уже существует, но мы ещё не указали, что это Альберт и что у него двадцать единиц маны. Пока числовое поле Mana равно 0, а Name содержит null — отсутствие строки. При включённой проверке nullable Visual Studio может предупредить, что поле Name не получило начального значения. Сейчас мы зададим его вручную, а в следующем уроке перенесём заполнение в момент создания объекта.
Сразу после 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. Однако у воина другой запас сил и другое действие, поэтому для него подготовим собственное описание. В Program.cs разместим класс Warrior после закрывающей скобки класса 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(), затем отдельно задаём имя и только после этого заполняем ману. С двумя полями такой код терпим, но при расширении класса строк станет больше, а одно из присваиваний легко забыть. С этой проблемы мы начнём следующий урок и познакомимся с конструктором.
Мы описали класс Mage: поля Name и Mana хранят данные мага, а метод CastSpell проверяет и расходует его ману. Затем дважды использовали new Mage() и получили два отдельных объекта. Альберт и Мерлин устроены одинаково, но значения полей у каждого свои.
Через точку мы обращаемся к доступному полю или методу объекта. Вызов albert.CastSpell() работает с Альбертом, а merlin.CastSpell() — с Мерлином. У воина другой класс и другое действие: в Warrior нет метода CastSpell, поэтому такую ошибку компилятор обнаружит ещё до запуска.
После создания героя мы пока отдельно заполняем каждое поле, и одно из присваиваний легко забыть. В следующем уроке конструктор позволит передавать начальные данные прямо при создании объекта. Вопрос о том, как ограничивать дальнейшее изменение открытых полей, тоже остаётся — к нему мы подойдём после конструктора.
Класс задаёт общие данные и действия объектов одного типа; поля хранят состояние каждого объекта, а методы описывают его поведение. Теперь разберём несколько коротких ситуаций, применим этот механизм в новой задаче и в конце проверим понимание в разговоре с AI.
Проверяем смысл: класс, поле и метод
Начнём с трёх ситуаций, в которых похожие формулировки приводят к разному пониманию кода.
Какое объяснение точнее всего описывает класс, поле и метод?
Какой смысл у записи public class Robot?
Какое правило важно сохранить при применении темы урока?
Собираем понятия в одну картину
Определения по отдельности уже знакомы. Теперь свяжите саму идею, её запись в C# и ограничение, о котором важно помнить.
Соотнесите элементы слева с подходящими объяснениями.
Применяем: класс, поле и метод
Теперь перенесём идею урока в новую небольшую ситуацию. Решение должно оставаться коротким, но по коду должно быть видно, зачем выбран именно этот механизм. Опишите класс Robot с полями Name и Energy и методом Work, который выводит имя робота. Создайте двух роботов, задайте им разные значения и вызовите метод у каждого.
Подсказка
Подсказка: Начните с главной записи урока: `public class Robot`. Затем проверьте, что код действительно показывает её смысл.
Объясняем: класс, поле и метод
Практическое решение готово. Осталось проверить, сможете ли вы объяснить его другому разработчику: что делает механизм, как читается запись и где проходит его граница.
Ответьте на четыре вопроса своими словами. AI может задать один уточняющий вопрос, если в ответе не хватает важной части.
Ответьте на вопросы собеседника и получите итоговую оценку по теме.
Проходной результат: 70%. Попыток: 3.
Начать собеседование