В прошлом уроке мы увидели предел процедурного решения. Имя и запас сил каждого героя хранились в отдельных переменных, действия находились в стороне от данных, а функция заклинания технически могла получить сведения о воине. Код работал, но правила нашего игрового мира существовали только в голове разработчика.

Мы уже наметили другое устройство: данные конкретного героя и подходящие ему действия объединяются в объекте, а их общий шаблон задаёт класс. Теперь перенесём эту модель в 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, а у WarriorAttack, поэтому вызов edward.CastSpell() компилятор отклонит ещё до запуска программы.

Сейчас после создания объекта мы вручную присваиваем значение каждому полю. В следующем уроке конструктор поможет задавать начальные данные непосредственно при создании объекта.

Практическая работа. Классы, поля и методы

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

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

Начнём с трёх ситуаций, в которых похожие формулировки приводят к разному пониманию кода.

Какое объяснение точнее всего описывает класс, поле и метод?

Какой смысл у записи public class Robot?

Какое правило важно сохранить при применении темы урока?

Собираем понятия в одну картину

Определения по отдельности уже знакомы. Теперь свяжите саму идею, её запись в C# и ограничение, о котором важно помнить.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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