Программа курса← Объектно ориентированное программирование
1История ООП
2Объект и Класс
3Базовые концепции ООП
4Принцип проектирования GRASP
5Принцип проектирования SOLID
6Паттерны GOF

Жизненный цикл объекта

1 · Создание 2 · Использование 3 · Уничтожение конструктор, память выделена поля меняются, методы работают ссылок больше нет, память освобождена

У объекта в программе есть начало и конец: он создаётся, работает и исчезает из памяти. Разберём все три этапа, а дольше всего задержимся на первом – именно там объект получает своё состояние, и именно там его проще всего испортить.

Ещё раз уточним: детали создания и удаления объектов в языках устроены по-разному. Где-то всё автоматизировано сборщиком мусора, а где-то часть объектов освобождается сама, а часть остаётся на вас – как в 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()) { "Модель обязательна" }
    }
}

Порядок инициализации

Стоит помнить, что до конца конструктора объект ещё не готов. Вызывать из него методы, которые наследник может переопределить, – известный источник ошибок: наследник получит управление раньше, чем его собственные поля будут заполнены.

Итог: конструктор отвечает не за «заполнить поля», а за то, чтобы некорректного объекта не существовало в принципе. Проверки собирайте в одном конструкторе, а остальные пусть вызывают его.