
- •А.А. Волосевич
- •1. Работа с Числами
- •2. Дата и время
- •3. Работа со строками и текстом
- •4. Преобразование информации
- •5. Отношения равенства и порядка
- •Сравнение для выяснения равенства
- •Сравнение для выяснения порядка
- •6. Жизненный цикл объектов
- •Алгоритм «сборки мусора»
- •Финализаторы и интерфейс iDisposable
- •7. Перечислители и итераторы
- •8. Интерфейсы стандартных коллекций
- •9. Массивы и класс system.Array
- •10. Типы для работы с коллекциями-списками
- •11. Типы для работы с коллекциями-множествами
- •12. Типы для работы с коллекциями-словарями
- •13. Типы для создания пользовательских коллекций
- •14. Технология linq to objects
- •1. Оператор условия Where().
- •2. Операторы проекций.
- •3. Операторы упорядочивания.
- •4. Оператор группировки GroupBy().
- •5. Операторы соединения.
- •6. Операторы работы с множествами.
- •7. Операторы агрегирования.
- •8. Операторы генерирования.
- •9. Операторы кванторов и сравнения.
- •10. Операторы разбиения.
- •11. Операторы элемента.
- •12. Операторы преобразования.
- •15. Работа с объектами файЛовой системы
- •16. Ввод и вывод информации
- •Потоки данных и декораторы потоков
- •2. Классы для работы с потоками, связанными с хранилищами.
- •3. Декораторы потоков.
- •4. Адаптеры потоков.
- •Адаптеры потоков
- •17. Основы xml
- •18. Технология linq to xml
- •Создание, сохранение, загрузка xml
- •Запросы, модификация и трансформация xml
- •Пространства имён xml
- •19. ДОполнительные возможности обработки xml
- •20. Сериализация
- •Сериализация времени выполнения
- •Сериализация контрактов данных
- •21. Состав и взаимодействие сборок
- •22. Метаданные и получение информации о типах
- •23. Позднее связывание и кодогенерация
- •24. Динамические типы
- •25. Атрибуты
- •26. Файлы конфигуРации
- •27. Основы мНогопоточноГо программирования
- •28. Синхронизация потоков
- •29. Библиотека параллельных расширений
- •Параллелизм на уровне задач
- •Параллелизм при императивной обработке данных
- •Параллелизм при декларативной обработке данных
- •Обработка исключений и отмена выполнения задач
- •Коллекции, поддерживающие параллелизм
- •30. Асинхронный вызов методов
- •31. Процессы и домены
- •32. Безопасность
- •Разрешения на доступ
- •Изолированные хранилища
- •Криптография
- •33. Диагностика
6. Жизненный цикл объектов
Все типы платформы .NET делятся на ссылочные типы и типы значений. Переменные типов значений создаются в стеке, и их время жизни ограничено тем блоком кода, в котором они объявляются. Например, если переменная типа значения объявлена в некотором методе, то после выхода из метода память в стеке, занимаемая переменной, автоматически освободится.
Переменные ссылочного типа (объекты) располагаются в динамической памяти – «управляемой куче» (managed heap). Размещение объектов в «куче» происходит последовательно. Для этого CLR поддерживает указатель на свободное место в куче, перемещая его на соответствующее количество байт после выделения памяти очередному объекту.
Алгоритм «сборки мусора»
Если при работе программы превышен некий порог расхода памяти, CLR запускает процесс, называемый сборка мусора1. На первом этапе сборки мусора строится граф используемых объектов. Отправными точками в построении графа являются корневые объекты. Это объекты следующих категорий:
-
локальная переменная или параметр выполняемого метода (а также всех методов в стеке вызова);
-
статическое поле;
-
объект в очереди завершения (этот термин будет разъяснён позже).
При помощи графа используемых объектов выясняется реально занимаемая этими объектами память. Затем происходит дефрагментация «кучи» ‑ используемые объекты перераспределяются так, чтобы занимаемая ими память составляла единый блок в начале «кучи». Ключевой особенностью сборки мусора является то, что она осуществляется автоматически и независимо от потоков выполнения приложения2.
При размещении и удалении объектов CLR использует ряд оптимизаций. Во-первых, объекты размером более 85 Кб размещаются в отдельной управляемой куче. При сборке мусора данная куча не дефрагментируется, так как копирование больших блоков памяти снижает производительность. Во-вторых, управляемая куча для малых объектов выделяет три поколения объектов – Gen0, Gen1 и Gen2. Вначале все объекты в куче относятся к Gen0. После первой сборки мусора те объекты, которые не были удалены, переходят в поколение Gen1, а новые объекты будут принадлежать Gen0. Вторая сборка мусора порождает поколение Gen2. Процесс сборки мусора работает с объектами старших поколений, только если освобождение памяти в младших поколениях дало неудовлетворительный результат.
Финализаторы и интерфейс iDisposable
Обсудим автоматическую сборку мусора с точки зрения программиста, разрабатывающего некий класс. С одной стороны, такой подход имеет свои преимущества. В частности, практически исключаются случайные утечки памяти, которые могут вызвать «забытые» объекты. С другой стороны, объект может захватить некоторые особо ценные ресурсы (например, подключения к базе данных), которые требуется освободить сразу после того, как объект перестаёт использоваться. В такой ситуации выходом является написание особого метода, который содержит код освобождения ресурсов.
Но как гарантировать освобождение ресурсов, даже если ссылка на объект была случайно утеряна? Класс System.Object содержит виртуальный метод Finalize(). Если объект пользовательского класса при работе захватывает ресурсы, класс может переопределить метод Finalize() для их освобождения. Объекты таких классов при сборке мусора обрабатываются особо. Когда сборщик мусора распознаёт, что уничтожаемый объект имеет собственную реализацию метода Finalize(), он помещает такой объект в специальную очередь завершения. Затем в отдельном программном потоке у объектов из очереди завершения происходит вызов метода Finalize() и фактическое уничтожение.
Язык C# не позволяет явно переопределить в пользовательском классе метод Finalize(). Вместо переопределения Finalize() в классе описывается специальный финализатор. Имя финализатора имеет вид ~<имя класса>, он не имеет параметров и модификаторов доступа (считается, что модификатор доступа финализатора ‑ protected, а значит, его нельзя явно вызвать у объекта). При наследовании в финализатор класса-наследника автоматически подставляется вызов финализатора класса-предка.
Рассмотрим пример класса с финализатором:
public class ClassWithFinalizer
{
public void DoSomething()
{
Console.WriteLine("I'm working...");
}
~ClassWithFinalizer()
{
Console.WriteLine("Bye!");
}
}
Приведём пример программы, использующей этот класс:
public class MainClass
{
private static void Main()
{
var A = new ClassWithFinalizer();
A.DoSomething();
// cборка мусора запуститься перед завершением приложения
}
}
Данная программа выводит следующие строки:
I'm working...
Bye!
Проблема с использованием финализатора заключается в недетерминированности его вызова. Программист может описать в классе некий метод, который следует вызывать «вручную», когда объект больше не нужен. Для унификации данного решения платформа .NET предлагает интерфейс IDisposable, содержащий единственный метод Dispose(), куда помещается завершающий код работы с объектом.
Правильный шаблон совместного использования финализатора и интерфейса IDisposable демонстрирует следующий класс:
public class DisposePattern : IDisposable
{
public void Dispose() // реализация IDisposable
{
Dispose(true);
// этот вызов подавляет запуск финализатора у объекта
GC.SuppressFinalize(this);
}
~DisposePattern() // реализация финализатора
{
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// вызов из Dispose() – полное контролируемое удаление
}
// этот блок выполняется только для вызова из финализатора
}
}
Язык C# имеет специальный оператор using, который гарантирует вызов метода Dispose() для объектов, используемых в своём блоке. Синтаксис оператора using следующий:
using (<имя объекта или объявление и создание объектов>) <блок кода>
Продемонстрируем использование оператора using:
using (var A = new DisposePattern())
{
A.DoSomething();
// компилятор поместит сюда вызов A.Dispose()
}
Сборщик мусора представлен статическим классом System.GC, который обладает несколькими полезными методами (это неполный список):
-
Collect() ‑ вызывает принудительную сборку мусора в программе.
-
GetGeneration() – возвращает поколение для указанного объекта;
-
SuppressFinalize() ‑ подавляет вызов финализатора для указанного объекта;
-
WaitForPendingFinalizers() ‑ приостанавливает текущий поток выполнения до тех пор, пока не будут выполнены все финализаторы освобождаемых объектов.