В данной статье я хочу осветить механизм исключений в C#, а также рассказать о жизненном цикле объектов, освобождении ресурсов в CLR и интерфейсе IDisposadle.
Обработка исключений
.NЕТ предлагает единую технику для обнаружения ошибок времени выполнения и передачи сообщений о них: это - структурированная обработка исключений (Structured Exception Handling, SEH).
В распоряжение всех разработчиков предоставляется единый и хорошо продуманный подход к обработке ошибок, который к тому же является общим для всех языков .NET. Разработчики также получают дополнительную возможность генерировать и перехватывать исключения между двоичными файлами и компьютерами в независимом от языка стиле.
Для того чтобы понять, как применять исключения в С#, в первую очередь необходимо осознать, что исключения в С# - это объекты. Все системные и пользовательские исключения в С# производятся от класса System.Exception (который, в свою очередь, производится от класса System,Object).В табл. 1. представлен перечень наиболее интересных свойств класса Exception.
Таблица 1. Главные члены класса System.Exceptlon
Свойство Назначение
HelpLink - Это свойство возвращает URL файла справки с описанием ошибки
Message - Это свойство (только для чтения) возвращает текстовое описание соответствующей ошибки. Само сообщение об ошибке устанавливается как параметр конструктора
Source - Возвращает имя объекта (или приложения), которое сгенерировало ошибку
StackTrace - Это свойство (только для чтения) возвращает последовательность вызовов; которые привели к возникновению ошибки
InnerException - Это свойство может быть использовано для сохранения сведений об ошибке между сериями исключений
Генерация исключения
Чтобы продемонстрировать использование System.Exception, мы обратимся к классу Car, и определим для него метод SpeedUp():
// В настоящее время SрееdUр()выводит сообщения об oшибках прямо на системную консоль
public void SpeedUp(int delta)
{
// Если машины больше нет, сообщить об этом
if(dead)
{ Console.WriteLineCpetName + " is out of order", ");}
else // Еще жива. можно увеличивать скорость
{
currSpeed += delta:
if(currSpeed >= maxSpeed)
{
Console.WriteLine(petName + " has overheated...");
dead = true:
}
else
Console.WriteLine("\tCurrSpeed = " + currSpeed):
}
}
Для наших целей мы переделаем метод SpeedUp( ) таким образом, чтобы при попытке ускорить уже вышедший из строя автомобиль (dead = = true) генерировалось исключение. Прежде всего вам потребуется создать и настроить новый объект класса Exception (исключение). Для передачи этого объекта используется ключевое слово throw. Вот пример:
// При попытке ускорить вьшедший из строя автомобиль будет сгенерировано исключение
public void SpeedUp(int dеlta)
{
if (dead)
throw new Exception("This саг is already dead");
else {...}
}
Прежде чем выяснить, как вызвать это исключение, необходимо отметить еще несколько моментов.
Во-первых, при создании пользовательского класса только мы сами принимаем решения о том, когда будут возникать исключения. 3десь мы создали исключение таким образом, что оно будет сгенерировано всякий раз при применении метода SpeedUp() к машине в нерабочем состоянии (dead= = true), принудительно прекращая выполнение этого метода.
Конечно, вы можете изменить метод SpeedUp() таким образом, что он будет восстанавливаться автоматически, без генерации каких-либо исключений. Как правило, исключения должны генерироваться только тогда, когда выполнение какого-либо метода должно быть немедленно прервано. При проектировании класса одним из самых важных моментов является принятие решений о том, когда должны генерироваться исключения.
Во-вторых, необходимо помнить, что в библиотеках среды выполнения .NET уже определено множество готовых исключений, которые можно и нужно использовать. Например, в пространстве имен System определены такие важные исключения, как АrgumеntОutОfRапgеЕхсерtiоn, IпdехОutОfRаngеЕхсеption, StackOverflowException и многие другие. В прочих пространствах имен определены свои исключения, относящиеся к тем областям, за которые отвечает соответствующее пространство имен.
Перехват исключений
Метод SpeedUp() готов сгенерировать исключение, однако кто перехватит это исключение и будет на него реагировать? Ответ прост: если мы создали код, генерирующий исключение, у нас должен быть блок try/catch для перехвата этого исключения. Этот блок может выглядеть, к примеру, так:
public static int Main(string[] args) {
// Создаем автомобиль
Саr budaha = new Car("Buddha". 100. 20);
// Пытаемся прибавить газ
try
{
for(int i = О; i < 10; i++)
{
buddha.SpeedUp(10);
}
}
catch(Exception е) // выводим сообщение и трассируем стек
{
Console.WriteLine(e.Message);
Console.WriteLine(e.StackTrace);
}
return 0;
}
В сущности, блок trу - это отрезок кода, во время выполнения которого происходит отслеживание возникающих исключений. При возникновении исключения выполнение кода, определенного в блоке try, прерывается, и начинает выполняться код, определенный в ближайшем следующем блоке catch. Если же в процессе выполнения кода в блоке try исключений так и не возникло, блок catch полностью пропускается и выполнение программы идет дальше согласно ее внутренней логике.
Обратите внимание, что в нашем случае мы явно указали в блоке catch тип исключения, с которым мы рассчитываем встретиться. Однако С# (как и все остальные языки программирования .NET) позволяет настроить блок catch так, чтобы он реагировал на любые исключения, вне зависимости от их типа. Такой блок catch выглядит следующим образом:
catch {Console.WriteLine("Something bad happened... ");}
Конечно же, подобная обработка исключений - это не лучший способ получить полную информацию о том, что произошло. С# предоставляет в ваше распоряжение мощные средства для работы с пользовательскими и системными исключениями.
Если нам надо обработать более одного исключения, при вызове исключений используем не один блок catch, а два, следующих друг за другом.
После блока try/catch в С# может следовать необязательный блок finally. Этот блок выполняется всегда, вне зависимости от того, сработало исключение или нет. Его главное назначение - гарантировать, что ресурсы, которые могут быть открыты потенциально опасным методом, будут обязательно освобождены. Этот блок, к примеру, используется для освобождения памяти, закрытия файла, отключения от источника и данных и выполнения прочих операций, связанных с корректным завершением программы.
Жизненный цикл объектов
В С# общий принцип управления памятью формулируется очень просто: для создания объекта в области «управляемой кучи» (managed heap) используется ключевое слово new. Среда выполнения .NET автоматически удалит объект тогда, когда он больше не будет нужен. Правило в целом вполне понятно, однако возникает один дополнительный вопрос: а как среда выполнения определяет, что объект больше не нужен? Короткий (то есть неполный) ответ гласит, что среда выполнения удаляет объект из памяти, когда в текущей области видимости больше не остается активных ссылок на этот объект.
Вне зависимости от того, насколько осторожно вы будете создавать объекты, как только место в управляемой куче заканчивается, автоматически запускается сборщик мусора (garbage collector, GC). Сборщик мусора оценивает все объекты, размещенные в настоящий момент в управляемой куче, с точки зрения того, есть ли в области видимости приложения активные ссылки на них. Если активных ссылок на какой -либо объект больше нет или объект установлен в nu11, этот объект помечается для удаления, и в скором времени память, занимаемая подобными объектами, высвобождается.
Сборщик мусора
Для хранения объектов CLR использует хип, подобный хипу C++, за тем важным исключением, что хип CLR не фрагментирован. Выделение объектов производится всегда один за другим в последовательных адресах памяти, что позволяет весьма существенно повысить производительность всего приложения (рис. 1-1). Когда память заканчивается, в дело вступает процесс, основная задача которого сводится к тому, чтобы освободить место в хипе путем дефрагментации неиспользуемой памяти.
Рис.1. Начальное состояние хипа. Рис. 2. Хип после завершения работы с некоторыми объектами.
Первое, что делает GC во время сборки мусора – это принимает решение о том, что все выделенные блоки памяти в вашей программе - это как раз и есть мусор, и они вам больше не нужны. К счастью, на этом GC не прекращает свою работу. Далее начинается утомительный поиск «живых» указателей на объекты по всем закоулкам приложения. Microsoft называет эти указатели «roots». В процессе поиска GC сканирует глобальную память программы (на рисунках обозначено как Global), стек (Stack – локальные переменные) и даже регистры процессора (CPU). Как мы знаем, каждый поток в программе имеет свой собственный стек, и CLR приходится сканировать их все. Кроме того, интересен тот факт, что CLR умеет работать даже с потоками, которые создавались в неуправляемом коде с использованием функций WinAPI.
Найдя «живой» указатель, сборщик мусора помечает объект, на который этот указатель указывает, как всё ещё используемый программой и запрашивает информацию о его структуре, чтобы в свою очередь просканировать уже этот объект на наличие указателей на другие объекты. И так далее, пока все указатели в программе не будут обработаны.
Теперь GC знает, какие объекты можно удалить, а какие пока не стоит. Сама по себе задача дефрагментации памяти является тривиальной, и все сложности начинаются позже, когда приходит время для корректировки всех указателей, которые существуют в программе, и присвоения им новых значений, получившихся после дефрагментации. Ну что же, GC только что просканировал всю память программы, почему бы не сделать это ещё раз...
Рис. 3. Хип после сборки мусора.
Алгоритмы работы GC построены во многом исходя из следующих правил. В частности, одно из таких правил утверждает, что только что созданные «молодые» объекты имеют наиболее короткое время жизни, а живущие уже давно будут жить ещё долго. Именно в соответствии с этим правилом в CLR существуют понятие поколений (generations). Объекты, выжившие после первой сборки мусора и дефрагментации, объявляются первым поколением. В следующий раз, те объекты из первого поколения, которые опять смогли выжить, перемещаются во второе поколение и уже больше не трогаются, выжившие объекты из нулевого поколения перемещаются в первое. Другими словами, объекты, пережившие две сборки мусора, остаются в хипе навсегда и GC не занимается их дефрагментацией.
Освобождение ресурсов в CLR
Наверняка вы уже знаете, что как таковых деструкторов у объектов в CLR нет. Компилятор C# просто переименовывает их в метод Finalize, который вызывается сборщиком мусора перед тем, как объект будет уничтожен или при завершении программы, независимо от того, существуют ссылки на данный объект или нет. При этом вы не знаете когда, в каком порядке и в каком потоке будут вызваны методы Finalize. Кроме того, они могут быть вообще не вызваны, например, если по какой-либо причине не будет вызван завершающий код процесса.
Для работы с объектами, имеющими метод Finalize, CLR использует следующий механизм. При создании объекта, если он содержит метод Finalize, ссылка на него помещается в специальный список, называемый Finalization Queue (рис. 4).
Рис.4. Управляемый хип и Finalization Queue. Рис. 5. Хип после завершения работы с объектами A, D, E, H.
После того, как GC определяет, что какой-либо объект можно удалить, ссылка на этот объект ищется в Finalization Queue, и если находится, то объект оставляется в покое до следующей сборки мусора, а ссылка на него из Finalization Queue удаляется и добавляется в другой список, называемый F-reachable Queue.
Рис. 6. Хип после первой сборки мусора.
Далее этим списком занимается специально созданный для этого поток, который по очереди вызывает методы Finalize для объектов из F-reachable Queue, а затем удаляет их и из этого списка.
Рис. 7. GC вызвал методы Finalize для объектов D Рис. 8. Хип после второй сборки мусора.
и H теперь они просто мусор.
Зачем нужны такие сложности? Дело в том, что по идее деструктор вызывается после того, как объект уже никто не использует, но нам никто не мешает прямо в деструкторе сохранить указатель на наш объект в какой-нибудь глобальной переменной и использовать его в дальнейшем без особых угрызений совести. При описанной выше схеме с объектом ничего не случится, и им можно будет спокойно пользоваться сколь угодно долго. Единственное отличие заключается в том, что после того, как наш объект станет не нужен, метод Finalize для него вызван уже не будет, так как ссылка на наш объект уже отсутствует в Finalization Queue. Но, как вы уже догадались, и эта ситуация исправима. Метод ReRegisterForFinalize, который мы используем в нашем примере, как раз и позволяет вернуть наш объект обратно в Finalization Queue.
Как видите, наличие деструкторов у объектов не предвещает ничего хорошего для вашей программы. Такие объекты не удаляются первой же сборкой мусора и у них больше шансов попасть во второе поколение и остаться в хипе навсегда, бесполезно занимая память, даже если они уже никому больше не нужны.
Всё это, конечно, не означает, что деструкторы – это зло, специально добавленное в CLR, чтобы усложнить нам жизнь. Ими можно и нужно пользоваться для освобождения unmanaged-ресурсов, таких, как файловые дескрипторы, соединения с БД и COM-объекты, но в тоже время нужно чётко понимать, что за этим стоит, и уж тем более не следует добавлять их к классам «просто так на всякий случай».
Интерфейс IDisposadle
Чтобы обеспечить единообразие методов, освобождающих ресурсы в разных классах, библиотеки классов .NET определяют интерфейс IDisposable, который содержит единственный член - метод Dispose();
publiC interface IDisposable
{ public void Dispose();}
Рекомендуется реализовывать этот интерфейс для всех классов, которые должны поддерживать явную форму освобождения ресурсов.
Используя такой подход, мы предоставляем пользователю возможность в любой момент высвобождать наиболее ценные ресурсы, не загружая дополнительную очередь завершения.
Класс System.GC
В заключение разберём класс System.GC, который предоставляет интерфейс к подсистеме сборки мусора. Мы не можем создавать экземпляры этого класса и не можем от него наследоваться, да это и не нужно, так как все методы и свойства этого класса статические и доступны для использования без создания объекта. Рассмотрим их все по порядку.
Это свойство возвращает максимальный номер поколения, который поддерживается системой. В настоящий момент это значение равно 2:
int MaxGeneration {get;}
Принудительный вызов сборщика мусора.
void Collect();
void Collect(int generation);
Параметр generation задаёт максимальный номер поколения, которое будет обработано при сборке мусора. Первый метод использует в качестве номера поколения максимальный номер и фактически соответствует следующему коду:
GC.Collect(GC.MaxGeneration);
Возвращает номер поколения, в котором находится заданный объект.
int GetGeneration(object obj);
int GetGeneration(WeakReference wo);
Возвращает число байт, занимаемых в данный момент управляемым хипом.
long GetTotalMemory(bool forceFullCollection);
Если параметр forceFullCollection равен true, то функция возвращает занимаемую хипом память после вызова сборщика мусора. Как мы уже знаем, GC не гарантирует освобождение абсолютно всей возможной памяти. Данная функция вызывает сборщик мусора несколько (до 20) раз до тех пор пока разница в занимаемой памяти до и после сборки не будет составлять 5%.
Предотвращает удаление объекта при сборке мусора, если даже на него нет уже ссылок.
void KeepAlive(object obj);
Подобная возможность может понадобиться, например, в случае если ссылки на ваш объект уже отсутствуют в управляемом коде, но ваш объект всё ещё необходим unmanaged коду.
Добавляет объект в Finalization Queue.
void ReRegisterForFinalize(object obj);
Удаляет объект из Finalization Queue.
void SuppressFinalize(object obj);
Останавливает текущий поток до завершения выполнения методов Finalize для всех объектов из F-reachable Queue
void WaitForPendingFinalizers();
Этот метод обычно используется совместно с GC.Collect. Например, следующий код удалит все объекты, которые больше не используются программой:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Комментариев нет:
Отправить комментарий