В прошлом уроке имя и мана мага лежали в отдельных переменных — 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# и ограничение, о котором важно помнить.

Соотнесите элементы слева с подходящими объяснениями.

класс, поле и метод
public class Robot
Типичная ошибка
Класс общий, но каждый объект получает собственное состояние; изменение поля одного объекта не меняет остальные.
Класс задаёт общие данные и действия объектов одного типа; поля хранят состояние каждого объекта, а методы описывают его поведение.
Объявляется открытый класс Robot, по описанию которого можно создавать отдельные объекты.

Применяем: класс, поле и метод

Теперь перенесём идею урока в новую небольшую ситуацию. Решение должно оставаться коротким, но по коду должно быть видно, зачем выбран именно этот механизм. Опишите класс Robot с полями Name и Energy и методом Work, который выводит имя робота. Создайте двух роботов, задайте им разные значения и вызовите метод у каждого.

Рабочая область
Подсказка

Подсказка: Начните с главной записи урока: `public class Robot`. Затем проверьте, что код действительно показывает её смысл.

Объясняем: класс, поле и метод

Практическое решение готово. Осталось проверить, сможете ли вы объяснить его другому разработчику: что делает механизм, как читается запись и где проходит его граница.

Ответьте на четыре вопроса своими словами. AI может задать один уточняющий вопрос, если в ответе не хватает важной части.

Ответьте на вопросы собеседника и получите итоговую оценку по теме.

Проходной результат: 70%. Попыток: 3.

Начать собеседование

Обсуждение урока

0
Комментарии видны всем. Чтобы участвовать в обсуждении, войдите или зарегистрируйтесь.
Модерация сообщества

Пожаловаться на комментарий

Расскажите модераторам, что именно требует внимания.