В прошлом уроке мы с вами уже научились создавать классы 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()
{
}
В его теле нет наших инструкций по заполнению полей, поэтому значения устанавливаются по умолчанию: например, 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# и ограничение, о котором важно помнить.
Соотнесите элементы слева с подходящими объяснениями.
Применяем: конструктор
Теперь перенесём идею урока в новую небольшую ситуацию. Решение должно оставаться коротким, но по коду должно быть видно, зачем выбран именно этот механизм. Создайте класс Book с полями Title и Pages. Добавьте конструктор с обязательным названием и количеством страниц по умолчанию 100, затем создайте две книги разными допустимыми способами.
Подсказка
Подсказка: Начните с главной записи урока: `new Book("Дюна", 500)`. Затем проверьте, что код действительно показывает её смысл.
Объясняем: конструктор
Практическое решение готово. Осталось проверить, сможете ли вы объяснить его другому разработчику: что делает механизм, как читается запись и где проходит его граница.
Ответьте на четыре вопроса своими словами. AI может задать один уточняющий вопрос, если в ответе не хватает важной части.
Ответьте на вопросы собеседника и получите итоговую оценку по теме.
Проходной результат: 70%. Попыток: 3.
Начать собеседование