Теория и задания по Си-Шарп (КФУ). Учебное пособие казань 2017 2 удк 681 06 ббк 32. 973 Печатается по постановлению Редакционноиздательского совета
![]()
|
ГЛАВА 9. СОЗДАНИЕ И УДАЛЕНИЕ ОБЪЕКТОВ В этой главе изучим, что происходит при создании объекта, как конструкторы инициализируют объекты и как используются деструкторы для уничтожения объектов. Узнаем, что происходит с объектом после уничтожения и как работает сборщик мусора. Использование конструкторов Конструкторы – это специальные методы, которые используются для инициализации объектов при создании. Если конструктор не описан в классе, то запускается конструктор по умолчанию. Процесс создания объекта проходит в два этапа, но записывается всё в одно выражение. Первый шаг – это выделение памяти, за это отвечает оператор new . Второй шаг – инициализация объекта при помощи конструктора, в этот момент полям класса присваиваются начальные значения либо те, которые указаны в конструкторе, либо значения по умолчанию (для чисел – это ноль, для ссылочных типов данных – это null). При создании объекта создаётся конструктор по умолчанию, если не создан свой. Иногда конструктор по умолчанию не подходит для инициализации объекта, тогда можно определить свой конструктор. Есть несколько причин, когда не подходит конструктор по умолчанию: не подходит доступ public, инициализация нулями неправильна, невидимый код тяжело понимать. Все поля, которые не определены в конструкторе, инициализируются нулями. Если конструктор отработал, то с объектом можно работать, если произошла ошибка в конструкторе, то объект не создался. Конструкторы, как и любые другие методы, можно перегрузить. class Overload { public Overload( ) { this.data = -1; } 66 public Overload(int x) { this.data = x; } private int data; } class Use { static void Main( ) { Overload o1 = new Overload( ); Overload o2 = new Overload(42); } } Если определен свой конструктор, то компилятор не создаст конструктор по умолчанию, и его придется прописать самостоятельно. При определении конструкторов можно использовать конструкцию, называемую списком инициализации. Определение одного конструктора при помощи вызова другого перегруженного конструктора. class Date { public Date() : this(1970, 1, 1) { } public Date(int year, int month, int day) { } } Ограничения – нельзя вызывать конструктор со списком инициализации из другого метода class Point { public Point(int x, int y) { ... } public void Init( ) : this(0, 0) { } // Ошибка при компиляции } 67 нельзя вызывать самого себя class Point { // Ошибка при компиляции public Point(int x, int y) : this(x, y) { } } нельзя использовать ключевое слово this в списке инициализации. class Point { // Ошибка при компиляции public Point( ) : this(X(this), Y(this)) { } public Point(int x, int y) { ... } private static int X(Point p) { ... } private static int Y(Point p) { ... } } При определении конструкторов необходимо определить константы и поля только для чтения. Поля, которые не могут быть переприсвоены, называются полями только для чтения. Есть три варианта инициализации полей только для чтения: • нулём неявно; • присвоением в конструкторе, что разрешено; • присвоением при объявлении поля в классе. class SourceFile { public SourceFile( ) { } private readonly ArrayList lines = new ArrayList( ); } Синтаксис конструкторов одинаков и для структур. Отличие в том, что для структур обязательно необходимо инициализировать все поля. Закрытый (private) конструктор – это особый конструктор экземпляров. Обычно он используется в классах, содержащих только статические элементы. 68 Если в классе один или несколько закрытых конструкторов и ни одного открытого конструктора, то прочие классы (за исключением вложенных классов) не смогут создавать экземпляры этого класса. Объявление пустого конструктора запрещает автоматическое создание конструктора по умолчанию. Стоит заметить, что если не использовать с конструктором модификатор доступа, то по умолчанию он все равно будет закрытым. Однако обычно используется модификатор private, чтобы ясно обозначить невозможность создания экземпляров данного класса. Закрытые конструкторы используются, чтобы не допустить создание экземпляров класса при отсутствии полей или методов экземпляра, например для класса Math, или когда осуществляется вызов метода для получения экземпляра класса. class Math { public static double Cos(double x) { ... } public static double Sin(double x) { ... } private Math( ) { ... } } class LessCumbersome { static void Main( ) { double answer = Math.Cos(42.0); } } Достоинства статичных методов – простота и быстрота, не надо создавать объект. Статичный конструктор вызывается при загрузке класса в память. class Example { static Example( ) { ... } } 69 Уничтожение объектов В приложении необходимо знать, что случается, когда объект выходит из области видимости или уничтожается. Процесс уничтожения объекта в C# состоит из двух шагов: деинициализация объекта и возвращение памяти в управляемую кучу. Отметим различия во времени жизни переменных структурных типов и объектов. Переменные структурного типа обычно имеют короткое время жизни, ограниченное блоком в котором они объявлены. Также их отличает детерминированное создание и уничтожение. Для объектов время жизни дольше и время уничтожения недетерминированное. В C# нельзя вручную удалить объект, так как эта возможность вызывала много различных ошибок в других языках: забывание удаления объектов, попытка уничтожения объекта дважды, удаление активного объекта. Сборщик мусора удаляет объекты за программиста. Сборщик гарантирует, что объекты будут удалены, и, причем, только один раз. Удаляются только недостижимые объекты. class Example { void Method(int limit) { for(int i = 0; i < limit; i++){ Example eg = new Example(); …. } // eg за пределами блока, существует ли он ещё? } } Как было указано выше, удаление объекта – двухшаговый процесс. На 70 первом шаге память очищается, на втором возвращается в кучу. Второй шаг одинаков для всех классов, а вот первый индивидуален для каждого класса. Для описания первого шага определяется деструктор или метод Finalize. Можно описать деструктор для определения очистки объекта. В С# нельзя вручную вызвать деструктор или метод Finalizе. В С# программист не знает, когда будет уничтожен объект, только известно, что это произойдёт после того, как он станет недостижимым. Порядок и время вызова деструкторов неопределенны. Желательно избегать деструкторов, так как он использует ресурсы сборщика мусора. Если в классе нет неуправляемых частей, то сборщик мусора сам уничтожит объекты и деструктор не нужен. Память от удаленного объекта освободится, когда сборщик мусора уничтожит объект, однако, кроме памяти объект может содержать другие ресурсы, которые лучше освободить быстрее: подключение к БД, открытые файловые потоки. Для этого используется метод Dispose, для его использования необходимо, чтобы класс определял интерфейс IDisposable, и описав метод Dispose, убедиться что метод не вызывается дважды. Оператор using определяет область видимости объект, в конце этого блока для объекта вызывается метод Dispose. Resource r1 = new Resource( ); try { r1.Test( ); } finally { if (r1 != null) ((IDisposable)r1).Dispose( ); } Рассмотрим более подробно как происходит работа с памятью в языке С#. При создании объекта память размещается в управляемой куче, и переменная хранит только ссылку на расположение объекта. Для типов в управляемой куче требуются служебные данные и при их размещении, и при их удалении функциональной возможностью автоматического управления памятью среды CLR, также известной как сборка мусора. Сборка мусора в 71 высокой степени оптимизирована, и в большинстве сценариев она не создает проблем с производительностью. Автоматическое управление памятью является одной из служб, которые предоставляет среда CLR во время управляемого выполнения. Сборщик мусора среды CLR управляет освобождением и выделением памяти для приложения. Для разработчиков это означает, что при разработке управляемого приложения не нужно писать код для управления памятью. Автоматическое управление памятью позволяет устранить распространенные проблемы, такие как не освобожденный по забывчивости объект, вызывающий утечку памяти, или попытки доступа к памяти для уже удаленного объекта. Выделение памяти. При инициализации нового процесса среда выполнения резервирует для него непрерывную область адресного пространства. Это зарезервированное адресное пространство называется управляемой кучей. Эта управляемая куча содержит указатель адреса, с которого будет выделена память для следующего объекта в куче. Изначально этот указатель устанавливается в базовый адрес управляемой кучи. Все ссылочные типы размещаются в управляемой куче. Когда приложение создает первый ссылочный тип, память для него выделяется, начиная с базового адреса управляемой кучи. При создании приложением следующего объекта сборщик мусора выделяет для него память в адресном пространстве, непосредственно следующем за первым объектом. Пока имеется доступное адресное пространство, сборщик мусора продолжает выделять пространство для новых объектов по этой схеме. Выделение памяти из управляемой кучи происходит быстрее, чем неуправляемое выделение памяти. Поскольку среда выполнения выделяет память для объекта путем добавления значения к указателю, это осуществляется почти так же быстро, как выделение памяти из стека. Кроме того, поскольку выделяемые последовательно новые объекты и располагаются последовательно в управляемой куче, приложение может получать доступ к объектам очень быстро. Освобождение памяти. Механизм оптимизации сборщика мусора определяет наилучшее время для выполнения сбора, основываясь на произведенных выделениях памяти. Когда сборщик мусора выполняет очистку, он освобождает память, выделенную для объектов, которые больше не используются приложением. Он определяет, какие объекты больше не используются, основываясь на корнях приложения. Каждое приложение имеет набор корней. Каждый корень либо ссылается на объект, находящийся в 72 управляемой куче, либо имеет значение NULL. Корни приложения содержат указатели глобальных и статических объектов, локальные переменные и параметры ссылочных объектов в стеке потока, а также регистры процессора. Сборщик мусора имеет доступ к списку активных корней, которые поддерживаются JIT-компилятором и средой выполнения. С помощью этого списка он проверяет корни приложения и в процессе проверки создает граф, содержащий все объекты, к которым можно получить доступ из этих корней. Объекты, не входящие в этот граф, являются недостижимыми из данных корней приложения. Сборщик мусора считает недостижимые объекты мусором и будет освобождать выделенную для них память. В процессе очистки сборщик мусора проверяет управляемую кучу, отыскивая блоки адресного пространства, занятые недостижимыми объектами. При обнаружении недостижимого объекта он использует функцию копирования памяти для уплотнения достижимых объектов в памяти, освобождая блоки адресного пространства, выделенные под недостижимые объекты. После уплотнения памяти, занимаемой достижимыми объектами, сборщик мусора вносит необходимые поправки в указатель, чтобы корни приложения указывали на новые расположения объектов. Он также устанавливает указатель управляемой кучи в положение после последнего достижимого объекта. Память уплотняется, только если при очистке обнаруживается значительное число недостижимых объектов. Если после сборки мусора все объекты в управляемой куче остаются на месте, то уплотнение памяти не требуется. Для повышения производительности среда выполнения выделяет память для больших объектов в отдельной куче. Сборщик мусора автоматически освобождает память, выделенную для больших объектов. Однако для устранения перемещений в памяти больших объектов эта память не сжимается. Поколения и производительность. С целью оптимизации производительности сборщика мусора управляемая куча подразделяется на три поколения: 0, 1 и 2. Сборщик мусора среды выполнения хранит новые объекты в поколении 0. Для созданных ранее объектов, оставшихся после сборок мусора, их уровень повышается, и они переводятся в поколения 1 и 2. Поскольку быстрее сжать часть управляемой кучи, чем всю кучу, эта схема позволяет сборщику мусора освобождать память в определенном поколении, а не освобождать память для всей кучи каждый раз при сборке мусора. В действительности сборщик мусора выполняет очистку при заполнении поколения 0. Если приложение пытается создать новый объект, когда поколение 0 заполнено, сборщик мусора обнаруживает, что в поколении 0 не 73 осталось свободного адресного пространства для объекта. Сборщик мусора выполняет сборку, пытаясь освободить для этого объекта адресное пространство в поколении 0. Сборщик мусора начинает проверять объекты в поколении 0, а не все объекты в управляемой куче. Это наиболее эффективный подход, поскольку, как правило, новые объекты имеют меньшее время жизни, и можно ожидать, что многие из объектов в поколении 0 к моменту проведения сборки мусора уже не используются приложением. Кроме того, сборка мусора только в поколении 0 зачастую освобождает достаточно памяти для того, чтобы приложение могло продолжить создавать новые объекты. После того, как сборщик мусора выполнит освобождение для поколения 0, он уплотняет память для достижимых объектов. Затем сборщик мусора повышает уровень этих объектов и считает эту часть управляемой кучи поколением 1. Поскольку объекты, оставшиеся после сборок мусора, как правило, имеют большее время жизни, имеет смысл повысить их уровень до более старшего поколения. В результате сборщику мусора не обязательно выполнять повторную проверку объектов поколений 1 и 2 при каждой сборке мусора в поколении 0. После того, как сборщик мусора выполнил свою первую очистку для поколения 0 и повысил уровень достижимых объектов до поколения 1, он считает оставшуюся часть управляемой кучи поколением 0. Он продолжает выделять память для новых объектов в поколении 0 до тех пор, пока оно не заполнится и не появится необходимость в следующей сборке мусора. В этот момент оптимизатор сборщика мусора определяет, есть ли необходимость проверки объектов в более старых поколениях. Например, если при очистке поколения 0 не освободится достаточно памяти для того, чтобы приложение смогло успешно завершить свою попытку создания нового объекта, сборщик мусора может выполнить очистку поколения 1, а затем - поколения 0. Если и при этом не освобождается достаточно памяти, он может выполнить очистку поколений 2, 1 и 0. После каждой сборки мусора сборщик уплотняет объекты в поколении 0 и продвигает их в поколение 1. Объекты поколения 1, оставшиеся после сборки мусора, продвигаются в поколение 2. Поскольку сборщик мусора поддерживает только три поколения, объекты поколения 2, оставшиеся после сборки мусора, остаются в этом поколении до тех пор, пока при очередной очистке они не будут определены, как недостижимые. Освобождение памяти для неуправляемых ресурсов. Для большинства объектов, созданных приложением, сборщик мусора автоматически выполнит 74 необходимые задачи по управлению памятью. Однако, для неуправляемых ресурсов требуется явная очистка. Основным типом неуправляемых ресурсов являются объекты, образующие упаковку для ресурсов операционной системы, такие как дескриптор файлов, дескриптор окна или сетевое подключение. Хотя сборщик мусора может отслеживать время жизни управляемого объекта, инкапсулирующего неуправляемые ресурсы, он не имеет определенных сведений о том, как освобождать эти ресурсы. При создании объекта, инкапсулирующего неуправляемый ресурс, рекомендуется предоставлять необходимый код для очистки неуправляемого ресурса в общем методе Dispose . Предоставление метода Dispose дает возможность пользователям объекта явно освобождать память при завершении работы с объектом. Вопросы к разделу 1. Как происходит создание и удаление объектов? 2. В каких случаях используются закрытые конструкторы? 3. Возможна ли перегрузка конструкторов и деструкторов? Приведите примеры. 4. Какой метод вызывает сборщик мусора даже, если память ещё не переполнена? 5. В чем смысл использования оператора using? Лабораторная работа Задания на создание конструкторов, деструкторов, обращение к сборщику мусора. Время, необходимое на выполнение задания 75 мин. Упражнение 9.1 В классе банковский счет, созданном в предыдущих упражнениях, удалить методы заполнения полей. Вместо этих методов создать конструкторы. Переопределить конструктор по умолчанию, создать конструктор для заполнения поля баланс, конструктор для заполнения поля тип банковского счета, конструктор для заполнения баланса и типа банковского счета. Каждый конструктор должен вызывать метод, генерирующий номер счета. Упражнение 9.2 Создать новый класс BankTransaction, который будет хранить информацию о всех банковских операциях. При изменении баланса 75 счета создается новый объект класса BankTransaction, который содержит текущую дату и время, добавленную или снятую со счета сумму. Поля класса должны быть только для чтения (readonly). Конструктору класса передается один параметр – сумма. В классе банковский счет добавить закрытое поле типа System.Collections.Queue , которое будет хранить объекты класса BankTransaction для данного банковского счета; изменить методы снятия со счета и добавления на счет так, чтобы в них создавался объект класса BankTransaction и каждый объект добавлялся в переменную типа System.Collections.Queue. Упражнение 9.3 В классе банковский счет создать метод Dispose, который данные о проводках из очереди запишет в файл. Не забудьте внутри метода Dispose вызвать метод GC.SuppressFinalize, который сообщает системе, что она не должна вызывать метод завершения для указанного объекта. Домашнее задание 9.1 В класс Song (из домашнего задания 8.2) добавить следующие конструкторы: 1) параметры конструктора – название и автор песни, указатель на предыдущую песню инициализировать null. 2) параметры конструктора – название, автор песни, предыдущая песня. В методе Main создать объект mySong. Возникнет ли ошибка при инициализации объекта mySong следующим образом: Song mySong = new Song(); ? Исправьте ошибку, создав необходимый конструктор. |