У объекта в программе есть начало и конец: он создаётся, работает и исчезает из памяти. Разберём все три этапа, а дольше всего задержимся на первом – именно там объект получает своё состояние, и именно там его проще всего испортить.
Ещё раз уточним: детали создания и удаления объектов в языках устроены по-разному. Где-то всё автоматизировано сборщиком мусора, а где-то часть объектов освобождается сама, а часть остаётся на вас – как в C++, где объект на стеке умирает при выходе из области видимости, а созданный через new живёт, пока его не удалят.
Три этапа жизни
1. Создание
- Объект создается на основе класса с помощью специального метода-конструктора.
- В этот момент выделяется память под объект, и его атрибутам присваиваются значения (initial values).
// Конструктор всегда называется именем класса
var myCar = new Car("красный", "Tesla");
// Поля, которым не задали значение, получают значение по умолчанию: 0, false, null
// Конструктор тоже носит имя класса
Car myCar("красный", "Tesla");
// А вот поле, которое не инициализировали, содержит мусор, а не ноль
// Конструкторов в Go нет: по соглашению их роль играет функция New…
myCar := NewCar("красный", "Tesla")
// Поля, которым не задали значение, всегда обнуляются: 0, "", nil
# Конструктор – это метод __init__
my_car = Car("красный", "Tesla")
# Атрибута не существует, пока ему не присвоили значение
// Конструктор объявляется словом constructor
const myCar = new Car("красный", "Tesla");
// Поле, объявленное без значения, равно undefined
// Конструктор всегда называется именем класса
Car myCar = new Car("красный", "Tesla");
// Поля объекта получают значения по умолчанию: 0, false, null
// Основной конструктор – это заголовок класса, слово new не нужно
val myCar = Car("красный", "Tesla")
// Свойство обязано получить значение, иначе код не скомпилируется
2. Использование
- Программа взаимодействует с объектом: читает его атрибуты и вызывает его методы.
- Значения атрибутов объекта могут меняться в процессе работы.
myCar.StartEngine(); // метод меняет состояние объекта
myCar.Color = "зелёный"; // свойство читают и записывают как поле
Console.WriteLine(myCar.Color);
myCar.StartEngine(); // метод меняет состояние объекта
myCar.set_color("зелёный"); // свойств нет – значение меняет метод
std::cout << myCar.color() << std::endl;
myCar.StartEngine() // метод меняет состояние объекта
myCar.Color = "зелёный" // экспортированное поле доступно напрямую
fmt.Println(myCar.Color)
my_car.start_engine() # метод меняет состояние объекта
my_car.color = "зелёный" # атрибут читают и записывают напрямую
print(my_car.color)
myCar.startEngine(); // метод меняет состояние объекта
myCar.color = "зелёный"; // поле читают и записывают напрямую
console.log(myCar.color);
myCar.startEngine(); // метод меняет состояние объекта
myCar.setColor("зелёный"); // поля закрыты – значение меняет сеттер
System.out.println(myCar.getColor());
myCar.startEngine() // метод меняет состояние объекта
myCar.color = "зелёный" // за присваиванием стоит сеттер свойства
println(myCar.color)
3. Уничтожение
- Когда объект больше не нужен, он удаляется из памяти. В одних языках (например, C++) это происходит вручную, в других (Python, Java, C#) за этим следит сборщик мусора (Garbage Collector).
Уничтожение устроено куда сложнее создания, и здесь мы его только обозначили. Поколения сборщика мусора, паузы на сборку, утечки через живые ссылки, ручное освобождение ресурсов – на собеседованиях под это отводят отдельный блок вопросов, а по объёму это тема самостоятельного курса, не этого.
Дальше – про первый этап подробно. Объект появляется в памяти пустым, и задача конструктора – сделать так, чтобы с первой же секунды жизни он был в корректном состоянии.
Зачем нужен конструктор
Можно создать объект, а потом расставить значения полей снаружи. Так делать не стоит, и причина не в красоте: между созданием и заполнением объект существует наполовину собранным. Если в этот момент кто-то вызовет его метод, поведение непредсказуемо.
Конструктор закрывает эту щель: пока он не отработал, объекта для внешнего кода не существует. Поэтому в него имеет смысл складывать всё, без чего объект бессмысленен.
Правило простое: если без значения объект нерабочий – оно должно прийти в конструктор. Остальное можно задать позже.
Проверки в конструкторе
Конструктор – последнее место, где ещё можно отказаться создавать объект. Если модель
машины пустая, лучше выбросить ошибку сразу, чем ловить null через десять вызовов в
другом конце программы.
Это тот же приём, что и с закрытыми полями из прошлого урока: сузить круг мест, где может возникнуть некорректное состояние.
Несколько конструкторов
Часто объект можно создать по-разному: с полным набором данных и с минимальным. Тогда появляется несколько конструкторов, и здесь легко нажить проблему – скопировать проверки в каждый.
Правильнее сделать один «главный» конструктор со всеми проверками, а остальные заставить вызывать его, подставляя значения по умолчанию.
Пример
В C# перегрузки связывают через : this(...)В Go конструкторов нет: их роль играют функции New…, и одна вызывает другуюВ Python перегрузок нет – используют значения по умолчанию у параметровВ TypeScript перегрузок конструктора по сути нет – используют необязательные параметрыВ Java перегрузки связывают через this(...)В Kotlin есть основной конструктор в заголовке класса и вторичные через constructor.
public class Car
{
// Главный конструктор: все проверки живут здесь
public Car(string color, string model)
{
if (string.IsNullOrWhiteSpace(model))
throw new ArgumentException("Модель обязательна", nameof(model));
Color = color;
Model = model;
}
// Перегрузка не дублирует проверки, а делегирует их главному
public Car(string model) : this("белый", model) { }
public string Color { get; set; }
public string Model { get; }
}
#include <stdexcept>
class Car {
public:
// Главный конструктор: все проверки живут здесь
Car(std::string color, std::string model)
: color_(std::move(color)), model_(std::move(model)) {
if (model_.empty()) {
throw std::invalid_argument("Модель обязательна");
}
}
// Делегирующий конструктор не дублирует проверки
explicit Car(std::string model) : Car("белый", std::move(model)) {}
const std::string& color() const { return color_; }
const std::string& model() const { return model_; }
private:
std::string color_;
std::string model_;
};
type Car struct {
Color string
Model string
}
// Главный «конструктор» – обычная функция. Ошибку возвращаем, а не паникуем.
func NewCar(color, model string) (*Car, error) {
if strings.TrimSpace(model) == "" {
return nil, errors.New("модель обязательна")
}
return &Car{Color: color, Model: model}, nil
}
// Вариант с умолчанием делегирует главному
func NewWhiteCar(model string) (*Car, error) {
return NewCar("белый", model)
}
class Car:
# Перегрузок в Python нет: значение по умолчанию решает ту же задачу
def __init__(self, model, color="белый"):
if not model or not model.strip():
raise ValueError("Модель обязательна")
self.color = color
self.model = model
class Car {
// Необязательный параметр вместо второй перегрузки
constructor(
public readonly model: string,
public color: string = "белый",
) {
if (!model.trim()) {
throw new Error("Модель обязательна");
}
}
}
public class Car {
private String color;
private final String model;
// Главный конструктор: все проверки здесь
public Car(String color, String model) {
if (model == null || model.isBlank()) {
throw new IllegalArgumentException("Модель обязательна");
}
this.color = color;
this.model = model;
}
// Перегрузка делегирует главному через this(...)
public Car(String model) {
this("белый", model);
}
}
class Car(val model: String, var color: String = "белый") {
// init выполняется сразу после основного конструктора
init {
require(model.isNotBlank()) { "Модель обязательна" }
}
}
Порядок инициализации
Стоит помнить, что до конца конструктора объект ещё не готов. Вызывать из него методы, которые наследник может переопределить, – известный источник ошибок: наследник получит управление раньше, чем его собственные поля будут заполнены.
Итог: конструктор отвечает не за «заполнить поля», а за то, чтобы некорректного объекта не существовало в принципе. Проверки собирайте в одном конструкторе, а остальные пусть вызывают его.