Полное руководство по языку программирования С# 14 и платформе .NET 10 / Сборка мусора, управление памятью и указатели

Финализируемые объекты

Начальный ⏱ 6 мин чтения 👁 2 🕓 обновлено 11.08.2026

Большинство объектов, используемых в программах на C#, относятся к управляемым или managed-коду. Такие объекты управляются CLR и легко очищаются сборщиком мусора. Однако вместе с тем встречаются также и такие объекты, которые задействуют неуправляемые объекты (подключения к файлам, базам данных, сетевые подключения и т.д.). Такие неуправляемые объекты обращаются к API операционной системы. Сборщик мусора может справиться с управляемыми объектами, однако он не знает, как удалять неуправляемые объекты. В этом случае разработчик должен сам реализовывать механизмы очистки на уровне программного кода.

Освобождение неуправляемых ресурсов подразумевает реализацию одного из двух механизмов:

  • Создание деструктора

  • Реализация классом интерфейса System.IDisposable

Создание деструкторов

Если вы вдруг программировали на языке C++, то наверное уже знакомы с концепцией деструкторов. Метод деструктора носит имя класса (как и конструктор), перед которым стоит знак тильды (~).

Деструкторы можно определить только в классах. Деструктор в отличие от конструктора не может иметь модификаторов доступа и параметры. При этом каждый класс может иметь только один деструктор.

Например, определим в классе Person простейший деструктор:


class Person
{
    public string Name { get;}
    public Person(string name) => Name = name;

    ~Person()
    {
        Console.WriteLine($"{Name} has deleted");
    }
}

В данном случае в деструкторе в целях демонстрации просто выводится строка на консоль, которая уведомляет, что объект удален. Но в реальных программах в деструктор вкладывается логика освобождения неуправляемых ресурсов.

Однако на деле при очистке сборщик мусора вызывает не деструктор, а метод Finalize. Все потому, что компилятор C# компилирует деструктор в конструкцию, которая эквивалентна следующей:


protected override void Finalize()
{
    try
    {
        // здесь идут инструкции деструктора
    }
	finally
	{
		base.Finalize();
	}
}

Метод Finalize уже определен в базовом для всех типов классе Object, однако данный метод нельзя так просто переопределить. И фактическая его реализация происходит через создание деструктора.

Используя в программе класс Person, после ее завершения можно будет увидеть на консоли сообщение об удалении объекта tom:


Test();        
GC.Collect();   // очистка памяти под объект tom
Console.Read(); // ставим задержку

void Test()
{
    Person tom = new Person("Tom");
}

public class Person
{
    public string Name { get;}
    public Person(string name) => Name = name;

    ~Person()
    {
        Console.WriteLine($"{Name} has been deleted");
    }
}

Обратите внимание, что даже после завершения метода Test и соответственно удаления из стека ссылки на объект Person в куче, может не последовать немедленного вызова деструктора. Лишь при завершении всей программы гарантировано произойдет очистка памяти. Однако с .NET 5 и в последующих версиях при завершении программы деструкторы не вызываются. Поэтому в программе выше для более быстрой очистки памяти применяется метод GC.Collect и для гарантированного вызова деструктора устанавливается задержка с помощью вызова Console.Read(), который ожидает от пользователя ввода.

На уровне памяти это выглядит так: сборщик мусора при размещении объекта в куче определяет, поддерживает ли данный объект метод Finalize. И если объект имеет метод Finalize, то указатель на него сохраняется в специальной таблице, которая называется очередь финализации. Когда наступает момент сборки мусора, сборщик видит, что данный объект должен быть уничтожен, и если он имеет метод Finalize, то он копируется в еще одну таблицу и окончательно уничтожается лишь при следующем проходе сборщика мусора.

Стоит отметить, что точное время вызова деструктора не определено. Кроме того, при финализации двух связанных объектов порядок вызова деструкторов не гарантируется. То есть если объект A хранит ссылку на объект B, и при этом оба эти объекта имеют деструкторы, то для объекта B деструктор моет уже отработать в то время, как для объекта A деструктор только начнет работу.

И здесь мы можем столкнуться со следующей проблемой: а что если нам немедленно надо вызвать деструктор и освободить все связанные с объектом неуправляемые ресурсы? В этом случае мы можем использовать второй подход - реализацию интерфейса IDisposable.

Интерфейс IDisposable

Интерфейс IDisposable объявляет один единственный метод Dispose, в котором при реализации интерфейса в классе должно происходить освобождение неуправляемых ресурсов. Например:


Test();

void Test()
{
    Person? tom = null;
    try
    {
        tom = new Person("Tom");
    }
    finally
    {
        tom?.Dispose();
    }
}

public class Person : IDisposable
{
    public string Name { get;}
    public Person(string name) => Name = name;

    public void Dispose()
    {
        Console.WriteLine($"{Name} has been disposed");
    }
}

В данном коде используется конструкция try...finally. По сути эта конструкция по функционалу в общем эквивалентна следующим двум строкам кода:


Person tom = new Person("Tom");
tom.Dispose();

Но конструкцию try...finally предпочтительнее использовать при вызове метода Dispose, так как она гарантирует, что даже в случае возникновения исключения произойдет освобождение ресурсов в методе Dispose.

Комбинирование подходов

Мы рассмотрели два подхода. Какой же из них лучше? С одной стороны, метод Dispose позволяет в любой момент времени вызвать освобождение связанных ресурсов, а с другой - программист, использующий наш класс, может забыть поставить в коде вызов метода Dispose. В общем бывают различные ситуации. И чтобы сочетать плюсы обоих подходов мы можем использовать комбинированный подход. Microsoft предлагает нам использовать следующий формализованный шаблон:


public class SomeClass: IDisposable
{
    private bool disposed = false;

    // реализация интерфейса IDisposable.
    public void Dispose()
    {
        // освобождаем неуправляемые ресурсы
        Dispose(true);
		// подавляем финализацию
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposed) return;
        if (disposing)
        {
            // Освобождаем управляемые ресурсы
        }
        // освобождаем неуправляемые объекты
        disposed = true;
    }

    // Деструктор
    ~SomeClass()
    {
        Dispose (false);
    }
}

Логика очистки реализуется перегруженной версией метода Dispose(bool disposing). Если параметр disposing имеет значение true, то данный метод вызывается из публичного метода Dispose, если false - то из деструктора.

При вызове деструктора в качестве параметра disposing передается значение false, чтобы избежать очистки управляемых ресурсов, так как мы не можем быть уверенными в их состоянии, что они до сих пор находятся в памяти. И в этом случае остается полагаться на деструкторы этих ресурсов. Ну и в обоих случаях освобождаются неуправляемые ресурсы.

Еще один важный момент - вызов в методе Dispose метода GC.SuppressFinalize(this). GC.SuppressFinalize не позволяет системе выполнить метод Finalize для данного объекта. Если же в классе деструктор не определен, то вызов этого метода не будет иметь никакого эффекта.

Таким образом, даже если разработчик не использует в программе метод Dispose, все равно произойдет очистка и освобождение ресурсов.

Общие рекомендации по использованию Finalize и Dispose

  • Деструктор следует реализовывать только у тех объектов, которым он действительно необходим, так как метод Finalize оказывает сильное влияние на производительность

  • После вызова метода Dispose необходимо блокировать у объекта вызов метода Finalize с помощью GC.SuppressFinalize

  • При создании производных классов от базовых, которые реализуют интерфейс IDisposable, следует также вызывать метод Dispose базового класса:

    public class Derived: Base
    {
        private bool IsDisposed = false;
    
        protected override void Dispose(bool disposing)
        {
            if (IsDisposed) return;
            if (disposing)
            {
    			// Освобождение управляемых ресурсов
    		}
            IsDisposed = true;
    		// Обращение к методу Dispose базового класса
            base.Dispose(disposing);
        }
    }
    
  • Отдавайте предпочтение комбинированному шаблону, реализующему как метод Dispose, так и деструктор

Проверь себя

📋 Тест по теме «Финализируемые объекты» Тест по теме

Практические задания

Лёгкое Ручная проверка Заполните пропуск в коде (1)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

class _____
{
    public string Name { get;}
    public Person(string name) => Name = name;

    ~Person()
    {
        Console.WriteLine($"{Name} has deleted");
    }
}
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (2)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

protected _____ void Finalize()
{
    try
    {
        // здесь идут инструкции деструктора
    }
	finally
	{
		base.Finalize();
	}
}
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (3)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

Test();        
GC.Collect();   // очистка памяти под объект _____
Console.Read(); // ставим задержку

void Test()
{
    Person tom = new Person("Tom");
}

public class Person
{
    public string Name { get;}
    public Person(string name) => Name = name;

    ~Person()
    {
        Console.WriteLine($"{Name} has been deleted");
    }
}
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (4)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

Test();

void Test()
{
    Person? _____ = null;
    try
    {
        tom = new Person("Tom");
    }
    finally
    {
        tom?.Dispose();
    }
}

public class Person : IDisposable
{
    public string Name { get;}
    public Person(string name) => Name = name;

    public void Dispose()
    {
        Console.WriteLine($"{Name} has been disposed");
    }
}
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (5)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

Person tom = new Person("_____");
tom.Dispose();
Войдите, чтобы отправить решение
Среднее Ручная проверка Восстановите код по памяти (1)
+20 XP

В этом фрагменте кода урока пропущены два ключевых элемента. Впишите оба пропущенных значения через запятую в порядке появления (например: x, 5).

class Person
{
    public string _____ { get;}
    public Person(string _____) => Name = name;

    ~Person()
    {
        Console.WriteLine($"{Name} has deleted");
    }
}
Войдите, чтобы отправить решение
Среднее Ручная проверка Восстановите код по памяти (2)
+20 XP

В этом фрагменте кода урока пропущены два ключевых элемента. Впишите оба пропущенных значения через запятую в порядке появления (например: x, 5).

protected _____ void Finalize()
{
    try
    {
        // здесь идут инструкции деструктора
    }
	finally
	{
		base.Finalize();
	}
}
Войдите, чтобы отправить решение
Сложное Ручная проверка Объясните и модифицируйте пример из урока «Финализируемые объекты»
+30 XP

Опишите своими словами, что делает приведённый пример кода из урока «Финализируемые объекты», и предложите одно осмысленное изменение (например, другое входное значение или дополнительное условие). Приведите итоговый код.

class Person
{
    public string Name { get;}
    public Person(string name) => Name = name;

    ~Person()
    {
        Console.WriteLine($"{Name} has deleted");
    }
}
Войдите, чтобы отправить решение
Сложное Ручная проверка Найдите потенциальную проблему в коде урока «Финализируемые объекты»
+30 XP

Изучите приведённый фрагмент кода и опишите, при каких входных данных или условиях он может сработать не так, как ожидается (граничный случай, отсутствие проверки и т.п.). Предложите исправленный вариант.

protected override void Finalize()
{
    try
    {
        // здесь идут инструкции деструктора
    }
	finally
	{
		base.Finalize();
	}
}
Войдите, чтобы отправить решение
Мини-проект Ручная проверка Мини-проект по теме «Финализируемые объекты»
+50 XP

Соберите небольшую программу, которая последовательно демонстрирует ключевые идеи урока. Программа должна быть рабочей и содержать минимум два самостоятельно написанных фрагмента, а не просто копию примера из урока.

Войдите, чтобы отправить решение
Домашнее задание Ручная проверка Домашнее задание по теме «Финализируемые объекты»
+25 XP

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

Войдите, чтобы отправить решение

Обсуждение (0)

Войдите, чтобы оставить комментарий.