В прошлом уроке мы с вами уже научились создавать классы Mage и Warrior, заводить объекты через new и обращаться к их полям через точку. И почти сразу столкнулись с бытовой проблемой: сам объект создаётся одной строкой, а его заполнение — отдельными командами.
Фёдор как раз занимается подготовкой персонажей для будущего сражения и показывает нам свой код:
Mage albert = new Mage();
albert.Name = "Альберт";
albert.Mana = 20;
Mage merlin = new Mage();
merlin.Name = "Мерлин";
merlin.Mana = 15;

С двумя полями это выглядит терпимо. Но Фёдор уже планирует добавить здоровье, уровень, экипировку. И чем больше полей появится, тем длиннее будет каждая «инициализация» нового персонажа.
Он предлагает начать с простого: сделать так, чтобы имя и начальный запас маны можно было задавать одной командой, не обращаясь к каждому полю по отдельности.
Ваша задача: измените класс Mage и код создания объектов так, чтобы имя и начальный запас маны одного героя задавались вместе. Используйте только уже знакомые возможности: поля, методы и параметры. После этого создайте Альберта с 20 маны и Мерлина с 15.
Если вы попробовали решить задачу средствами, которые у нас уже есть, то, скорее всего, перенесли присваивания в отдельный метод. Это естественный ход: мы уже умеем объединять действия и передавать в них данные.
Например, класс 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()
{
}
В полях уже находятся значения по умолчанию: в Mana — 0, в Name — null, то есть отсутствие строки. Пустой конструктор их не меняет. Поэтому раньше мы отдельно задавали имя и ману после создания объекта.
Как только мы объявили свой конструктор с параметрами, компилятор перестал добавлять вариант без параметров. Поэтому теперь запись без аргументов не работает:
Mage mage = new Mage(); // ошибка
Теперь при создании мага нужно передать строку и число. Но конструктор пока только присваивает их полям: new Mage("", -100) тоже допустим и создаст героя с пустым именем и отрицательной маной. Мы сделали передачу значений обязательной, а проверять их будем в следующем уроке.
Однако в реальных задачах бывает, что способов создания объекта нужно несколько. Например, Фёдору понадобился «служебный» маг для тестов — без имени, но с фиксированными значениями. Тогда мы можем добавить второй конструктор. Ниже показаны только поля и конструкторы; метод CastSpell остаётся в классе без изменений:
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 маны. Тогда в конструкторе с параметрами можно заменить int mana на int mana = 50. Для этого варианта уберём отдельный конструктор без параметров из предыдущего примера; метод CastSpell снова оставим без изменений:
public Mage(string name, int mana = 50)
{
Name = name;
Mana = mana;
}
Теперь можно создавать объекты так:
Mage merlin = new Mage("Мерлин"); // мана = 50
Mage albert = new Mage("Альберт", 20);
Здесь mana стал необязательным параметром: если его не передать, конструктор получит 50. Обязательный параметр name стоит перед ним и всё ещё требует аргумента, поэтому в этом варианте класса new Mage() недопустим.
Не стоит переоценивать роль конструктора. Он задаёт начальное состояние объекта, но не защищает его от последующих изменений:
Mage mage = new Mage("Мерлин", 50);
mage.Name = "";
mage.Mana = -100;
Такой код скомпилируется, потому что поля остаются открытыми. Конструктор с обязательными параметрами требует передать начальные данные, но наши открытые поля по-прежнему можно изменить после создания объекта. К этому вопросу мы вернёмся в следующем уроке.
Сначала мы объединили заполнение полей в метод Initialize, но его всё ещё можно было забыть вызвать. Конструктор связывает начальное заполнение с созданием объекта: аргументы передаются прямо в new Mage(...). Его имя совпадает с именем класса, а возвращаемый тип в объявлении не пишется — даже void.
Когда мы объявили конструктор с параметрами, компилятор перестал добавлять неявный вариант без параметров. Теперь допустимые способы создания задаём мы сами: можем добавить отдельный конструктор для тестового мага или сделать параметр маны необязательным, указав mana = 50. Это разные варианты устройства класса, и при изменении примера нужно учитывать, какие конструкторы в нём остались.
Пока наш конструктор только записывает полученные значения, а открытые поля можно менять и после создания. Пустое имя и отрицательная мана всё ещё допустимы для такого кода. В следующем уроке добавим проверки и разберёмся, как выполнять их при изменении данных.
Конструктор — специальная часть класса, которая вызывается при создании объекта и задаёт его начальное состояние. Теперь разберём несколько коротких ситуаций, применим этот механизм в новой задаче и в конце проверим понимание в разговоре с AI.
Проверяем смысл: конструктор
Начнём с трёх ситуаций, в которых похожие формулировки приводят к разному пониманию кода.
Какое объяснение точнее всего описывает конструктор?
Какой смысл у записи new Book("Дюна", 500)?
Какое правило важно сохранить при применении темы урока?
Собираем понятия в одну картину
Определения по отдельности уже знакомы. Теперь свяжите саму идею, её запись в C# и ограничение, о котором важно помнить.
Соотнесите элементы слева с подходящими объяснениями.
Применяем: конструктор
Теперь перенесём идею урока в новую небольшую ситуацию. Решение должно оставаться коротким, но по коду должно быть видно, зачем выбран именно этот механизм. Создайте класс Book с полями Title и Pages. Добавьте конструктор с обязательным названием и количеством страниц по умолчанию 100, затем создайте две книги разными допустимыми способами.
Подсказка
Подсказка: Начните с главной записи урока: `new Book("Дюна", 500)`. Затем проверьте, что код действительно показывает её смысл.
Объясняем: конструктор
Практическое решение готово. Осталось проверить, сможете ли вы объяснить его другому разработчику: что делает механизм, как читается запись и где проходит его граница.
Ответьте на четыре вопроса своими словами. AI может задать один уточняющий вопрос, если в ответе не хватает важной части.
Ответьте на вопросы собеседника и получите итоговую оценку по теме.
Проходной результат: 70%. Попыток: 3.
Начать собеседование