В прошлом уроке мы с вами уже научились создавать классы Mage и Warrior, заводить объекты через new и обращаться к их полям через точку. И почти сразу столкнулись с бытовой проблемой: сам объект создаётся одной строкой, а его заполнение — отдельными командами.

Фёдор как раз занимается подготовкой персонажей для будущего сражения и показывает нам свой код:

Mage albert = new Mage();
albert.Name = "Альберт";
albert.Mana = 20;

Mage merlin = new Mage();
merlin.Name = "Мерлин";
merlin.Mana = 15;

Фёдор по отдельности задаёт имя и запас маны каждому новому магу

С двумя полями это выглядит терпимо. Но Фёдор уже планирует добавить здоровье, уровень, экипировку. И чем больше полей появится, тем длиннее будет каждая «инициализация» нового персонажа.

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

Ваша задача: измените класс Mage и код создания объектов так, чтобы имя и начальный запас маны одного героя задавались вместе. Используйте только уже знакомые возможности: поля, методы и параметры. После этого создайте Альберта с 20 маны и Мерлина с 15.

Обсуждение гипотезыНаставник поможет уточнить мысльПопытка 1 из 3
Наставник

Предложите свою версию. Здесь не нужен идеальный ответ: важно показать ход мысли. Если понадобится, я задам один наводящий вопрос.

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

Например, класс Mage мог выглядеть так:

public class Mage
{
    public string Name;
    public int Mana;

    public void Initialize(string name, int mana)
    {
        Name = name;
        Mana = mana;
    }

    public void CastSpell()
    {
        if (Mana >= 5)
        {
            Mana -= 5;
            Console.WriteLine($"{Name} произносит заклинание! Осталось маны: {Mana}");
        }
        else
        {
            Console.WriteLine($"{Name}: недостаточно маны!");
        }
    }
}

И тогда создание мага превращается в два шага:

Mage albert = new Mage();
albert.Initialize("Альберт", 20);

Это уже лучше: мы действительно объединили два присваивания в одну операцию и перестали дублировать код.

Объект создаётся отдельно от его заполнения, и между этими действиями нет жёсткой связи. Метод Initialize можно забыть вызвать:

Mage merlin = new Mage();
// забыли задать имя и ману

Объект всё равно будет создан, и программа не заметит, что обязательный шаг пропущен. Правило «у мага должно быть имя и начальный запас маны» никак не закреплено в самом классе.

Нам нужен способ, при котором начальные данные становятся частью самого создания объекта.

Когда создание и заполнение — это одно действие

В C# для этого существует специальный механизм — конструктор.

Прежде чем писать конкретный код, давайте зафиксируем его общую идею. Конструктор объявляется внутри класса, имеет имя, совпадающее с именем класса, и не имеет возвращаемого типа — даже void не пишется. Он может принимать параметры, как обычный метод, но вызывается автоматически в момент создания объекта через new.

То есть мы описываем, какие данные нужны для создания объекта, и связываем это напрямую с выражением new.

Перепишем класс Mage, убрав метод Initialize и добавив конструктор:

public class Mage
{
    public string Name;
    public int Mana;

    public Mage(string name, int mana)
    {
        Name = name;
        Mana = mana;
    }

    public void CastSpell()
    {
        if (Mana >= 5)
        {
            Mana -= 5;
            Console.WriteLine($"{Name} произносит заклинание! Осталось маны: {Mana}");
        }
        else
        {
            Console.WriteLine($"{Name}: недостаточно маны!");
        }
    }
}

Теперь создание мага выглядит так:

Mage albert = new Mage("Альберт", 20);

Мы передаём аргументы прямо в круглых скобках после имени класса. Именно здесь вызывается конструктор: он получает "Альберт" и 20, а затем записывает их в поля нового объекта.

В отличие от обычного метода, конструктор нельзя вызвать через точку у уже созданного объекта:

albert.Mage("Альберт", 20); // ошибка

Конструктор вызывается только как часть выражения new в момент создания объекта.

Аргументы передаются по порядку и по типам. Конструктор ожидает сначала string, потом int, поэтому, если поменять их местами, компилятор не сможет подобрать подходящий вариант:

Mage gandalf = new Mage(30, "Гэндальф"); // ошибка

Если мы создадим несколько магов, каждый вызов new Mage(...) запустит конструктор заново и создаст отдельный объект со своими значениями:

Mage albert = new Mage("Альберт", 20);
Mage merlin = new Mage("Мерлин", 15);

albert.CastSpell();
merlin.CastSpell();

Это та же независимость объектов, о которой мы говорили в прошлом уроке: каждый маг живёт своей жизнью и хранит собственные данные.

Точно так же работает и класс воина:

public class Warrior
{
    public string Name;
    public int Stamina;

    public Warrior(string name, int stamina)
    {
        Name = name;
        Stamina = stamina;
    }

    public void Attack()
    {
        if (Stamina >= 5)
        {
            Stamina -= 5;
            Console.WriteLine($"{Name} наносит удар! Осталось выносливости: {Stamina}");
        }
        else
        {
            Console.WriteLine($"{Name}: недостаточно выносливости!");
        }
    }
}
Warrior edward = new Warrior("Эдвард", 25);
edward.Attack();

Теперь обязательные начальные значения и для магов, и для воинов передаются непосредственно при создании объектов.

Дополнительные варианты создания

Отсюда напрашивается вопрос: почему раньше такой код работал?

Mage mage = new Mage();

Хотя никакого конструктора мы не писали.

Дело в том, что если в классе нет ни одного явно объявленного конструктора, компилятор добавляет его автоматически. Для нашего класса это эквивалентно такому варианту:

public Mage()
{
}

В его теле нет наших инструкций по заполнению полей, поэтому значения устанавливаются по умолчанию: например, int получает 0, а строка — null. Именно поэтому раньше приходилось вручную задавать имя и ману после создания объекта.

Как только мы объявили свой конструктор с параметрами, компилятор перестал добавлять вариант без параметров. Поэтому теперь запись без аргументов не работает:

Mage mage = new Mage(); // ошибка

И это как раз то поведение, которого мы добивались: нельзя создать мага без обязательных данных.

Однако в реальных задачах бывает, что способов создания объекта нужно несколько. Например, Фёдору понадобился «служебный» маг для тестов — без имени, но с фиксированными значениями. Тогда мы можем добавить второй конструктор:

public class Mage
{
    public string Name;
    public int Mana;

    public Mage()
    {
        Name = "Безымянный маг";
        Mana = 50;
    }

    public Mage(string name, int mana)
    {
        Name = name;
        Mana = mana;
    }
}

Теперь доступны оба варианта:

Mage testMage = new Mage();
Mage albert = new Mage("Альберт", 20);

Компилятор выбирает подходящий конструктор по количеству и типам аргументов.

Иногда различие между вариантами ещё меньше. Допустим, мы решили, что у всех магов по умолчанию 50 маны. Тогда можно задать значение прямо в параметре:

public Mage(string name, int mana = 50)
{
    Name = name;
    Mana = mana;
}

Теперь можно создавать объекты так:

Mage merlin = new Mage("Мерлин");      // мана = 50
Mage albert = new Mage("Альберт", 20);

Параметры со значениями по умолчанию всегда располагаются после обязательных, поэтому new Mage() по-прежнему недопустим.

Не стоит переоценивать роль конструктора. Он задаёт начальное состояние объекта, но не защищает его от последующих изменений:

Mage mage = new Mage("Мерлин", 50);
mage.Name = "";
mage.Mana = -100;

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

Теперь вы умеете создавать объекты так, чтобы обязательные данные передавались сразу при создании, а не заполнялись отдельными командами.

Конструктор объявляется внутри класса, его имя совпадает с именем класса, а возвращаемый тип не указывается:

public Mage(string name, int mana)
{
    Name = name;
    Mana = mana;
}

Выражение new Mage(...) создаёт объект и сразу вызывает конструктор, передавая в него аргументы по порядку и по типам. Сам конструктор нельзя вызвать у уже созданного объекта — он работает только как часть создания.

Если в классе нет ни одного конструктора, компилятор добавляет конструктор без параметров. После объявления собственного конструктора он автоматически не создаётся.

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

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

Практическая работа. Конструктор класса

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

Проверяем смысл: конструктор

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

Какое объяснение точнее всего описывает конструктор?

Какой смысл у записи new Book("Дюна", 500)?

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

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

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

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

конструктор
new Book("Дюна", 500)
Типичная ошибка
Конструктор — специальная часть класса, которая вызывается при создании объекта и задаёт его начальное состояние.
Конструктор вызывается только при создании объекта через new; позднее состояние меняют полями, свойствами или методами.
Ключевое слово new создаёт объект Book и вызывает подходящий конструктор с названием и числом страниц.

Применяем: конструктор

Теперь перенесём идею урока в новую небольшую ситуацию. Решение должно оставаться коротким, но по коду должно быть видно, зачем выбран именно этот механизм. Создайте класс Book с полями Title и Pages. Добавьте конструктор с обязательным названием и количеством страниц по умолчанию 100, затем создайте две книги разными допустимыми способами.

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

Подсказка: Начните с главной записи урока: `new Book("Дюна", 500)`. Затем проверьте, что код действительно показывает её смысл.

Объясняем: конструктор

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

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

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

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

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

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

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

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

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