Конструктор это код, который выполняется при создании объекта через new. У него то же имя, что у класса, нет возвращаемого типа, а его задача привести новый объект в корректное состояние до того, как кто-то начнёт его использовать.
Вывод:
Mug: 8.50
Rejected: price
Поскольку конструктор отвергает отрицательную цену, Product с отрицательной ценой существовать не может. Эта гарантия главная причина писать конструкторы, а не задавать поля снаружи.
Конструктор по умолчанию и когда он исчезает
Если класс не объявляет ни одного конструктора, компилятор добавляет открытый конструктор без параметров, который делает только одно: выполняет инициализаторы полей. Поэтому new BankAccount() работает для класса, в котором есть только поля.
Как только вы объявляете любой конструктор, неявный больше не создаётся:
class Product
{
public string Name;
public Product(string name) { Name = name; }
}
var p = new Product(); // error CS7036: There is no argument given that corresponds to the required parameter 'name'
Это удивляет, когда конструктор без параметров нужен сериализатору, ORM или обобщённому ограничению new(). Если нужны оба, объявите конструктор без параметров сами:
public Product() { } // or
public Product() : this("Unnamed") { } // delegate to the other one (next section)
Конструктор может быть public, internal, protected или private. Его уровень доступа решает, кто может создавать через него объекты.
Перегрузка и цепочки через this(...)
У класса может быть несколько конструкторов с разными списками параметров. Чтобы не копировать одни и те же присваивания в каждый, объединяйте их в цепочку: : this(...) вызывает другой конструктор того же класса до выполнения текущего тела.
Вывод:
new Pizza():
full constructor: medium, classic, 0
size-only constructor body
parameterless constructor body
Result: medium classic with 0 toppings
Цепочка сначала выполняет самый полный конструктор и возвращается к тому, который вы вызвали. Держите настоящую работу (проверки, присваивания) в одном конструкторе, а остальным оставьте только подстановку значений по умолчанию.
Альтернатива это необязательные параметры: public Pizza(string size = "medium", string crust = "classic", int toppings = 0) даёт один конструктор, который покрывает все три вызова. Цепочки всё же лучше подходят, когда коротким конструкторам нужно что-то вычислить или когда изменение значения по умолчанию не должно требовать перекомпиляции вызывающего кода (значения необязательных параметров копируются в вызывающий код при компиляции).
Вызов конструктора базового класса через base(...)
Производный класс не наследует конструкторы. Каждый конструктор производного класса должен сначала выполнить конструктор базового класса. Если ничего не написать, компилятор вставит вызов конструктора базового класса без параметров; если такого нет, нужно выбрать конструктор через : base(...).
Порядок, в котором всё выполняется, стоит увидеть хотя бы раз:
Вывод:
Truck field initializer
Vehicle field initializer
Vehicle constructor body
Truck constructor body
KL-204, 3 axles
Сначала выполняются инициализаторы полей, производного класса раньше базового, а затем тела конструкторов, от базового вниз. Поэтому к моменту выполнения тела Truck поле Plate уже задано. Единственная ловушка в этом порядке: если конструктор базового класса вызывает virtual-метод, который переопределён в Truck, переопределение выполнится до тела конструктора Truck и увидит Axles всё ещё равным 0. Не вызывайте виртуальные методы из конструкторов.
Статические конструкторы
Статический конструктор инициализирует тип, а не объект. У него нет параметров и модификатора доступа, а среда выполнения вызывает его ровно один раз, непосредственно перед первым использованием типа.
Вывод:
Program started
Loading tax table (runs once)
123.00
119.00
Среда выполнения делает статический конструктор потокобезопасным: даже если несколько потоков одновременно обращаются к типу, он выполняется один раз. Если он выбросит исключение, каждое последующее использование типа будет выбрасывать TypeInitializationException до конца жизни процесса, поэтому делайте его простым и без ввода-вывода, который может завершиться ошибкой.
Конструктор или инициализатор объекта
Инициализатор объекта, new Pizza("large") { Toppings = 3 }, не является вторым конструктором. Компилятор превращает его в «выполнить конструктор, затем присвоить эти члены». Используйте каждый для того, в чём он хорош:
- Параметры конструктора для значений, без которых объект не может быть корректным. Каждый вызывающий обязан их передать, а конструктор может их проверить.
- Инициализатор для необязательных настроек с разумными значениями по умолчанию.
var order = new Order(customerId: 42) { Note = "Leave at the door", GiftWrap = true };
Начиная с C# 11 свойство с пометкой required должно быть задано в инициализаторе, что даёт синтаксису инициализаторов часть гарантий конструктора. Это описано на странице о свойствах.
Закрытые конструкторы
Конструктор private означает, что создавать экземпляры может только сам класс. Его используют два шаблона. Статический фабричный метод, при котором класс управляет созданием и может вернуть кэшированный объект или null, и одиночка (singleton):
Вывод:
21
100
Ошибка компилятора для new Temperature(5m) вне класса это CS0122, та же, что модификаторы доступа дают для любого закрытого члена. Именованные фабричные методы снимают реальное ограничение: два конструктора не могут оба принимать один decimal, а FromCelsius и FromFahrenheit могут. Класс, в котором только статические члены (служебный класс), следует объявить static, а не прятать его конструктор.
Конструкторы с телом-выражением
Конструктор, тело которого состоит из одного оператора, может использовать =>:
public Product(string name) => Name = name;
С кортежами можно даже присвоить несколько полей в одной строке: public Point(int x, int y) => (X, Y) = (x, y);.
Первичные конструкторы (C# 12)
C# 12 позволяет классу или структуре объявлять параметры конструктора прямо у типа. Параметры видны в каждом члене:
public class Customer(string name, int id)
{
public string Name { get; } = name; // copy into a property
public string Label => $"#{id} {Name}"; // or read a parameter directly
public Customer(string name) : this(name, 0) { } // other constructors must chain to it
}
Два отличия от записей удивляют людей. Параметры первичного конструктора класса не становятся открытыми свойствами; открывать их нужно самостоятельно. А параметр, используемый внутри члена, захватывается в скрытое поле, которое остаётся изменяемым, поэтому name = "x"; внутри метода компилируется. Первичные конструкторы лучше всего подходят для внедрения зависимостей, где параметры это сервисы, которые класс только вызывает:
public class OrderService(IOrderRepository repo, ILogger<OrderService> log)
{
public Order Get(int id) => repo.Find(id);
}
Частые ошибки
- Потеря конструктора без параметров. Добавление конструктора с параметрами убирает неявный (CS7036 на каждом
new X()). - Забытый
: base(...), когда у базового класса нет конструктора без параметров. Компилятор не может вставить неявный вызов и сообщает, что нет аргумента для обязательного параметра конструктора базового класса. - Вызов виртуальных методов в конструкторе. Переопределение выполняется до тела конструктора производного класса.
- Тяжёлая работа в конструкторе. Сетевые вызовы, чтение файлов или что-то медленное делают объекты дорогими, а исключения трудными в обработке. Для такой работы используйте статический фабричный метод или асинхронный метод инициализации.
- Указание возвращаемого типа.
public void Product()это обычный метод с именемProduct, и компилятор отвергает его, потому что член не может называться так же, как объемлющий тип (CS0542).
Часто задаваемые вопросы
Что такое конструктор в C#?
Конструктор это особый метод, который выполняется при создании объекта через new. У него то же имя, что у класса, и нет возвращаемого типа: public Product(string name) { Name = name; }. Его задача оставить новый объект в корректном состоянии, обычно присваивая поля из параметров и отвергая плохие входные данные.
Создаёт ли C# конструктор по умолчанию автоматически?
Только когда класс не объявляет ни одного конструктора. Тогда компилятор добавляет открытый конструктор без параметров, который оставляет каждое поле со значением из инициализатора или по умолчанию. Как только вы пишете любой конструктор, например принимающий имя, неявный пропадает, и new Product() перестаёт компилироваться (ошибка CS7036). Если он всё ещё нужен, объявите public Product() { } сами.
Как вызвать один конструктор из другого в C#?
Используйте : this(...) после списка параметров конструктора: public Product(string name) : this(name, 0m) { }. Сначала выполняется целевой конструктор, затем тело того, который вы вызвали. Чтобы вызвать конструктор базового класса, используйте : base(...) так же.
Что такое статический конструктор в C#?
Конструктор с пометкой static, без параметров и без модификатора доступа, который выполняется один раз на тип перед созданием первого экземпляра или первым обращением к статическому члену. Его используют для инициализации статических полей, которым нужно больше, чем однострочный инициализатор. Вызвать его самостоятельно нельзя, а если он выбросит исключение, тип станет непригодным до конца работы программы (TypeInitializationException).
Что использовать, конструктор или инициализатор объекта?
Используйте параметры конструктора для значений, без которых объект не может быть корректным, потому что компилятор заставляет каждого вызывающего их передать, а конструктор может их проверить. Используйте инициализатор объекта (new Product("Mug") { Color = "blue" }) для необязательных настроек. Инициализатор выполняется после завершения конструктора.
Что такое первичные конструкторы в C# 12?
Параметры, записанные прямо в объявлении класса, class Customer(string name, int id) { ... }, доступные всем членам класса. В отличие от позиционных параметров записи, они не становятся открытыми свойствами: открывайте их явно через public string Name => name; или public string Name { get; } = name;.