Лабораторная работа № 6 Управление исключительными ситуациями и «очисткой мусора» 1. ЦЕЛЬ РАБОТЫ

advertisement
-1–
Лабораторная работа № 6
Управление исключительными ситуациями и «очисткой
мусора»
1. ЦЕЛЬ РАБОТЫ
Целью работы является освоение практики применения исключений, их обработки,
а также изучение их взаимодействия со сборщиком мусора System.GC .
2. СОСТАВ РАБОЧЕГО МЕСТА
2.1. Оборудование: IBM-совместимый персональный компьютер (ПК).
2.2. Программное обеспечение: операционная система Windows, интегрированная
оболочка Visual Studio 2005 с установленным компонентом Visual C#.
3. КРАТКИЕ ТЕОРЕТИЧЕСКИЕ СВЕДЕНИЯ
Обработка исключений
Для того чтобы понять, как применять исключения в С#, в первую очередь необходимо осознать, что исключения в С# — это объекты. Все системные и
пользовательские исключения в С# производятся от класса System.Exception (который, в
свою очередь, производится от класса System.Object).
Главные члены класса System.Exception
Свойство
Назначение
HelpLink
Это свойство возвращает URL файла справки с описанием ошибки
Message
Это свойство (только для чтения) возвращает текстовое описание
соответствующей ошибки. Само сообщение об ошибке устанавливается как
параметр конструктора
Source
Возвращает имя объекта (или приложения), которое сгенерировало ошибку
StackTrace
Это свойство (только для чтения) возвращает последовательность вызовов,
которые привели к возникновению ошибки
InnerException
Это свойство может быть использовано для сохранения сведений об
ошибке между сериями исключений
Генерация исключения
Чтобы продемонстрировать использование System. Excepti on, мы обратимся к
классу Саг, который уже использовался в этой главе, а точнее, к его методу SpeedUpC).
Вот текущая реализация этого метода:
// В настоящее вреня SpeedUp() выводит сообщения об ошибках прямо на системную
консоль
public void SpeedUp(int delta)
{
// Если машины больше нет. сообщить об этом
If(dead)
{
Console.WriteLine(petName + " is out of order...");
-2–
}
else // Еще жива, можно увеличивать скорость
{
currSpeed += delta;
if(currSpeed >= maxSpeed)
{
Console. WriteLine(petName + " has overheated...");
dead = true;
}
else
Console. WriteLine(“\tCurrSpeed = " + currSpeed);
}
}
Для наших целей мы переделаем метод SpeedUpC ) таким образом, чтобы при
попытке ускорить уже вышедший из строя автомобиль (dead = = true) генерировалось
исключение. Прежде всего вам потребуется создать и настроить новый объект класса
Exception (исключение). Для передачи этого объекта используется ключевое слово throw.
Вот пример:
// При попытке ускорить вышедший из строя автомобиль будет сгенерировано
исключение
public void SpeedUp(int delta)
{
if (dead)
throw new Except ion ("This car Is already dead");
else
{
}
}
Прежде чем выяснить, как вызвать это исключение, необходимо отметить еще
несколько моментов.
Во-первых, при создании пользовательского класса только мы сами принимаем решения о
том, когда будут возникать исключения. Здесь мы создали исключение таким образом, что
оно будет сгенерировано всякий раз при применении метода SpeedUp О к машине в
нерабочем состоянии (dead = = true), принудительно
прекращая выполнение этого метода.
Конечно, вы можете изменить метод SpeedUpC ) таким образом, что он будет
восстанавливаться автоматически, без генерации каких-либо исключений. Как правило,
исключения должны генерироваться только тогда, когда выполнение какого-либо метода
должно быть немедленно прервано. При проектировании класса одним из самых важных
моментов является принятие решений о том, когда должны генерироваться исключения.
Во-вторых, необходимо помнить, что в библиотеках среды выполнения .NET уже
определено множество готовых исключений, которые можно и нужно использовать.
Например, в пространстве имен System определены такие важные исключения, как
ArgimentOutOfRangeException, IndexOutOfRangeException, StackQverflowException и
многие другие. В прочих пространствах имен определены свои исключения, относящиеся
к тем областям, за которые отвечает соответствующее пространство имен. В пространстве
имен System . Drawi ng . Pri nti ng определены исключения, которые могут возникнуть в процессе вывода на печать, в System . 10 — исключения вводавывода и т. п.
-3–
Перехват исключений
Метод SpeedUp() готов сгенерировать исключение, однако кто перехватит это
исключение и будет на него реагировать? Ответ прост: если мы создали код,
генерирующий исключение, у нас должен быть блок try/catch для перехвата этого
исключения. Этот блок может выглядеть, к примеру, так:
// Безопасно разгоняем автомобиль
public static int Main(string[] args)
{
// Создаем автомобиль
Car buddha - new Car(“Buddha", 100, 20);
// Пытаемся прибавить газ
{
try
for(int i = 0; i < 10; i++)
{
buddha.SpeedUp(10);
}
}
catch(Exception e) // Выводим сообщение и трассируем стек
{
Console.WrlteLine(e.Message};
Console.Wrltellne(e.StackTrace);
}
return 0;
}
В сущности, блок try — это отрезок кода, во время выполнения которого
происходит отслеживание возникающих исключений. При возникновении исключения
выполнение кода, определенного в блоке try, прерывается, и начинает выполняться код,
определенный в ближайшем следующем блоке catch. Если же в процессе выполнения кода
в блоке try исключений так и не возникло, блок catch полностью пропускается и
выполнение программы идет дальше согласно ее внутренней логике.
Создание пользовательских исключений, первый этап
Несмотря на то что мы вполне можем ограничиться в нашем приложении только
перехватом объектов класса System.Exception, часто бывает удобно создать свой
собственный класс-исключение с необходимыми нам членами. Например, предположим,
что мы создаем пользовательское исключение для знакомой нам ситуации с разгоном
вышедшего из строя автомобиля. Первое, что нам нужно сделать, — определить новый
класс, производный от System. Exception (по умолчанию для классов — пользовательских
исключений используется суффикс Exception). Далее мы можем определить все
необходимые нам свойства, методы и поля, которые будут использоваться внутри блока
catch. Мы также можем: заместить любые виртуальные члены, определенные в базовом
классе System.Except Ion:
// Это пользовательское исключение более подробно описывает ситуацию выхода машины
// из строя
public class CarlsDeadException : System.Exception
{
// С помощью этого исключения мы сможем получить имя несчастливой машины
private string carName;
public CarIsDeadException(){}
public CarlsDeadExceptionCstring carName)
{
-4–
this.carName = carName;
}
// Замещаем свойство Exception.Message
public override string Message
I
get
{
string msg = base.Message:
if(carName != null)
msg += carName + " has bought the farm...";
return msg;
}
}
}
Теперь наш класс-исключение CarlsDeadException содержит закрытую переменную
carName для хранения информации об имени машины, для которой было сгенерировано
исключение. Мы также добавили в класс два конструктора и заместили свойство Message
таким образом, чтобы включить в описание исключения имя машины. Синтаксис
генерации этого исключения очевиден:
// Генерируем пользовательское исключение
public void SpeedUp(int delta)
{
// Если машина вышла из строя, сообщаем об этом
if(dead)
{
// Генерируем исключение
throw new CarlsDeadException(thls.petName);
}
else // Машина еще жива, можно разгоняться
{
currSpeed += delta;
if(currSpeed >= maxSpeed)
{
dead = true;
}
else
Console. WriteLine( "tCurrSpeed = {0)". currSpeed);
}
}
Перехват этого исключения также не представит проблемы:
try
{
…
}
catch(CarIsDeadException e)
{
Console. WnteLine(e. Message);
Console. WriteLine(e.StackTrace);
}
Создание пользовательских исключений, второй этап
-5–
Наш пользовательский класс CarlsDeadExcepti on для выдачи сообщения об ошибке
замещает свойство Message. Кроме того, в этом классе также предусмотрен конструктор,
принимающий имя автомобиля, который стал причиной возникновения исключения. Мы
можем создать любой пользовательский класс- исключение, удовлетворяющий нашим
потребностям: это право разработчика. Однако в этом случае класс-исключение
CarlsDeadExcepti on при тех же возможностях может выглядеть проще:
public class CarIsDeadException : System.Exception
{
// Конструкторы для создания пользовательского сообщения об ошибке
public CarIsDeadException(){}
public CarlsDeadExceptlon(string message)
: base(message){}
public CarlsDeadException(string message. Exception innerEx)
: baseCmessage. innerEx){}
}
Обратите внимание, что в этом варианте нам не нужна строковая переменная для
хранения имени машины и мы не замещаем свойство Message. Все, что нам нужно — это
просто передать информацию членам базового класса. При генерации исключения мы
передаем необходимую информацию как параметр конструктора (результат выполнения
программы будет таким же, как и раньше):
SpeedUp(int delta)
{
…
// Если машина вышла из строя - сообщить об этом
if (dead)
{
// Передаем имя машины и сообщение как аргументы конструктора
throw new CarlsDeadExceptior(this.petNarne + " has bought the farm!");
}
else // Машина пока жива, можно разгоняться
{
…
}
}
Обработка нескольких исключений
В наиболее простой форме одному блоку try соответствует один блок catch. Однако
в реальных проектах часто возникают ситуации, когда нам необходимо отслеживать
возникновение не одного, а нескольких исключений. Например, предположим, что наш
метод SpeedUpO будет генерировать одно исключение, когда мы пытаемся разогнать
вышедший из строя автомобиль (эту ситуацию мы уже разбирали), и другое — когда мы
передаем этому методу неподходящие параметры (например, любое число меньше нуля).
// Проверка параметров на соответствие условиям
public void SpeedUp(int delta)
{
// Ошибка в принимаемом параметре? Генерируем системное исключение
If(delta < 0)
throw new ArgumentOutOfRangeException(“Must be greater than zero");
}
…
}
// Если машина вышла из строя - сообщить об этом
-6–
if(dead)
{
// Генерируем исключение CarlsDeauException
throw new CarIsDeadException(this,petName + " has bought the farm!");
Код для вызова исключений может выглядеть следующим образом:
// Теперь мы готовы перехватить оба исключения
try
{
for(int i = 0; i < 10: i++)
buddha.SpeedUp(10):
}
catch(CarIsDeadException e)
{
Console.WriteLine(e. Message) ;
Console.WriteLine(e.StackTrace);
}
catch(ArgumentOfRangeException e)
{
Console.WriteLine(e. Message);
Console.WriteLine(e.StackTrace);
}
Блок finally
После блока try /catch в С# может следовать необязательный блок f i n a l l y . Этот
блок выполняется всегда, вне зависимости от того, сработало исключение или нет. Его
главное назначение — гарантировать, что ресурсы, которые могут быть открыты
потенциально опасным методом, будут обязательно освобождены. Например, представим
себе, что наша задача — сделать так, чтобы радио в автомобиле выключалось всегда при
выходе из программы (метода MainO), вне зависимости от того, возникли или нет какиенибудь ошибки в процессе выполнения:
// Используем блок finally для закрытия всех ресурсов
public static int Main(string[] args)
{
Car buddha - new Car(“Buddha", 100. 20);
buddha. CrankTunes(true);
// Давим на газ
try
{
/7 Разгоняем машину. . .
)
catch(Car!sDeadExcept1on e)
{
Consol e . WriteLine(e .Message) :
Console. WriteLine(e.StackTrace):
}
catch(ArgumentOutofRangeException e)
{
Console. WritelLine(e. Message):
Console. WriteLine(e.StackTrace):
}
finally
{
-7–
// Этот блок будет выполнен всегда - вне зависимости от того,
// произошла ошибка или нет
buddha.CrankTunes( false) ;
}
return 0;
}
В нашей программе радио в автомобиле будет выключено всегда, вне зависимости
от возникновения каких-либо исключений — потому, что мы поместили
соответствующий код в блок finally . Конечно, с помощью блока finally можно не только
«выключать радио» — в реальных проектах этот блок используется для освобождения
памяти, закрытия файла, отключения от источника и данных и выполнения прочих
операций, связанных с корректным завершением программы.
Жизненный цикл объектов
В С# общий принцип управления памятью формулируется очень просто: для
создания объекта в области «управляемой кучи» (managed heap) используется ключевое
слово new. Среда выполнения .NET автоматически удалит объект тогда, когда он больше
не будет нужен. Правило в целом вполне понятно, однако возникает один
дополнительный вопрос: а как среда выполнения определяет, что объект больше не
нужен? Короткий (то есть неполный) ответ гласит, что среда выполнения
удаляет объект из памяти, когда в текущей области видимости больше не остается
активных ссылок на этот объект.
Предположим, что в вашем приложении создано (размещено в оперативной
памяти) три объекта класса Саг. Если в управляемой куче достаточно места, мы получим
три активные ссылки — по одной на каждый объект в оперативной памяти. Каждая такая
активная ссылка на объект в памяти называется также корнем (root).
Если вы занимаетесь только тем, что создаете все новые и новые объекты, в конце
концов пространство в управляемой куче закончится. В ситуации, когда свободного места
в управляемой куче больше нет, а вы пытаетесь создать новый объект, будет
сгенерировано исключение QutOfMemoryException. Поэтому, если вы хотите создать код
приложения, совершенно исключив возможность возникновения ошибок, создавать
объекты можно следующим образом (в реальных приложениях так обычно не поступают):
// Создаем объекты Саr таким образом, чтобы отреагировать на возможную нехватку
места
// в управляемой куче
public static int Main(string[] args)
{
…
Car yetAnotherCar;
try
(
yetAnotherCar = new Car();
}
catch (OutOfMemoryException e)
{
Console.WriteLine(e. Message):
Console.WriteLine( "Managed heap is FULL! Running GC...");
}
return 0;
}.
Вне зависимости от того, насколько осторожно вы будете создавать объекты, как
только место в управляемой куче заканчивается, автоматически запускается сборщик
-8–
мусора (garbage collector, GC). Сборщик мусора оценивает все объекты, размещенные в
настоящий момент в управляемой куче, с точки зрения того, есть ли в области видимости
приложения активные ссылки на них. Если активных ссылок на какой-либо объект больше
нет или объект установлен в nul 1 , этот объект помечается для удаления, и в скором
времени память, занимаемая подобными объектами, высвобождается.
Завершение ссылки на объект
Та схема управления памятью, которая была рассмотрена выше, обладает одной
важной особенностью, которая имеет как положительные, так и отрицательные стороны:
она работает полностью автоматически. С одной стороны, это упрощает процесс
программирования. С другой — нас может не устраивать то, что процесс удаления
объектов (закрытия соединения с базой данных, окна Windows и т. п.) будет происходить
в соответствии с неизвестным нам алгоритмом. Например, если
тип Саг устанавливает в процессе выполнения соединение с удаленным компьютером,
скорее всего, мы захотим, чтобы это соединение разрывалось в соответствии с
установленными нами правилами.
Если вам нужно обеспечить возможность удаления объектов из оперативной
памяти в соответствии с определенными вами правилами, первое, о чем вам необходимо
позаботиться — о реализации в вашем классе метода System.Object.Finalize( ) .
Заметим между прочим, что реализация этого метода по умолчанию (в базовом
классе) ничего не дает. Однако, как это ни странно, в С# запрещено напрямую замещать
метод Object.Finalize ( ) . Более того, вы даже не сможете вызвать в вашем приложении
этот метод напрямую! Если вы хотите, чтобы ваш пользовательский класс поддерживал
метод FinalizeO, вы должны использовать в определении этого класса метод, очень
похожий на деструктор C++:
public class Car : Object
(
// Деструктор С#?
~Car()
(
// Закрывайте все открытые объектом ресурсы!
// Далее в С# будет автоматически вызван метод Base.Finalize()
}
…
}
Если у вас есть опыт работы с C++, то подобный синтаксис вам покажется
знакомым. В C++ деструктор класса — это специальный метод класса, имя которого
выглядит как имя класса, перед которым стоит символ тильды (~). В C++ гарантируется,
что этот метод будет вызван, когда ссылка на объект выходит за пределы области
видимости (для типов, размещенных в стеке) или к объекту
применяется оператор delete (для объектов, размещенных в области динамической
памяти).
При размещении объекта С# в управляемой куче при помощи оператора new среда
выполнения автоматически определяет, поддерживает ли ваш объект метод Finalize()
(представленный в С# с помощью «деструктороподобного» синтаксиса). Если этот метод
поддерживается объектом, ссылка на этот объект помечается как «завершаемая»
(finalizable). При этом в специальной внутренней очереди «завершения» (finalization
queue) помещается указатель на данный объект. Когда сборщик мусора приходит к
выводу, что наступило время удалять данный объект из оперативной памяти, он
обращается к этому указателю и запускает деструктор С#, определенный для этого класса,
прежде чем будет произведено физическое удаление объекта из памяти
-9–
Создание метода удаления для конкретного случая
Еще раз предположим, что объекты в нашем приложении используют
определенные ресурсы. Конечно, все эти ресурсы освобождаются при помощи
встроенных или пользовательских деструкторов (о которых было рассказано в
предыдущем разделе). Однако существуют некоторые особо ценные ресурсы, например
подключения к базам данных (за каждое подключение приходится платить), которые
хотелось бы освобождать немедленно, а не в соответствии с расписанием работы
сборщика мусора. Поэтому проблему можно сформулировать так: каким образом можно
освободить ресурс, занятый объектом класса, немедленно, не дожидаясь естественной
смерти этого объекта? Наиболее очевидный способ — использовать для этого
специальный метод.
В С# метод, принудительно освобождающий интересующие пользователя ресурсы
объекта, принято называть DisposeO (освободить). Этот метод пользователь объекта будет
вызывать вручную, сразу же по завершении использования этого объекта и задолго до
того, как объект выйдет за пределы области видимости и будет помечен как подлежащий
физическому удалению из памяти (завершению).
Таким образом, можно гарантировать освобождение ресурсов без помещения указателя на
деструктор в очередь завершения. Кроме того, в этом случае освобождение ресурсов
будет произведено немедленно, а не тогда, когда у сборщика мусора «дойдут руки» до
нашего объекта
Взаимодействие со сборщиком мусора
Как и все в мире .NET, сборщик мусора — это объект, и мы можем обращаться к
нему через ссылку на объект. Для работы со сборщиком мусора в С# предназначен
специальный класс — System.GC (от garbage collector — сборщик мусора). Этот класс
определен как sealed, то есть производить от него другие классы при помощи
наследования невозможно. В System . GC определен небольшой набор статических
членов, при помощи которых и осуществляется взаимодействие со сборщиком мусора.
Самые важные из этих членов представлены в табл.
Некоторые члены типа System.GC
Член
Назначение
Collect()
Заставляет сборщик мусора заняться выполнением своих обязанностей для всех поколений (о
поколениях — чуть ниже). По желанию можно указать в качестве параметра конкретное
поколение
GetGeneration()
Возвращает поколение, к которому относится данный объект
MaxGeneration
Возвращает максимальное количество поколений, поддерживаемое данной системой
ReRegisterForFinallze()
Устанавливает флаг возможности завершения для объектов, которые ранее были помечены как
незавершаемые при помощи метода SupressFinalize()
SuppressFlnalize()
Устанавливает флаг запрещения завершения для объектов, которые в противном случае могли
бы быть завершены сборщиком мусора
GetTotalMemory()
Возвращает количество памяти (в байтах), которое в настоящее время занимают объекты в
управляемой куче, включая те объекты, которые будут вскоре удалены. Этот метод принимает
параметр типа boolean, с помощью которого можно указать, запускать или нет процесс сборки
мусора при вызове этого метода
Оптимизация сборки мусора
При знакомстве с членами класса System. GC мы уже встречались с понятием
поколения. Поколение (generation) — это еще одна концепция, имеющая назначение
сделать процесс сборки мусора в .NET более удобным.
Когда сборщик мусора .NET помечает объекты для завершения, обычно он не
проверяет все подряд объекты приложения: это заняло бы слишком много времени,
особенно для больших (то есть реальных) приложений. Для того чтобы повысить
- 10 –
производительность сборки мусора, все объекты в управляемой куче разбиты на группы
— поколения. Смысл такой группировки прост: чем дольше
объект существует в управляемой куче, тем больше вероятность того, что он будет нужен
и в дальнейшем. В качестве примера можно привести объект самого приложения — он
появляется при запуске приложения и удаляется лишь при завершении его работы. В то
же время существует значительная вероятность того, что недавно появившиеся объекты
быстро перестанут быть нужными (например, временные объекты, определенные внутри
области видимости метода). Основываясь на этой концепции, каждый объект относится к одному из следующих поколений:
• Поколение 0: недавно появившиеся объекты, которые еще не проверялись сборщиком
мусора.
• Поколение 1: объекты, которые пережили одну проверку сборщика мусора (они были
помечены для удаления, но не удалены физически, поскольку
в управляемой куче было достаточно свободного места).
• Поколение 2: объекты, которые пережили более чем одну проверку сборщика мусора.
При очередном запуске процесса сборки мусора сборщик в первую очередь
производит проверку и удаление всех объектов поколения 0. Если при этом освободилось
достаточно места, выжившие объекты поколения 0 переводятся в поколение 1 и на этом
процесс сборки мусора заканчивается. Если же после проверки поколения 0 места все еще
недостаточно, запускается процесс проверки объектов поколения 1, а затем (при
необходимости) — и поколения 2. Таким образом, за счет концепции поколений недавно
созданные объекты обычно удаляются быстрее, чем
объекты -ее историей».
Мы вполне можем определять, к какому поколению относится тот или иной
объект, непосредственно в процессе выполнения программы. Для этого используется
метод GC.GetGeneration(). Кроме того, метод GC.Collect () позволяет нам указать
поколение, которое будет проверяться при вызове сборщика мусора.
4. ПОРЯДОК ВЫПОЛНЕНИЯ РАБОТЫ
Порядок выполнения работы:
1. Составить программу для работы с классами и объектами, демонстриру
использование исключений и «сборки муссора» по одному из вариантов, приведенных в
следующей таблице. Вводимые значения и результаты вывести на экран дисплея.
№ варианта Описание действия программы
1
Создать класс с несколькими объектами (не меньше 3). Сгенерировать
исключение при применении метода, взаимодействующего с 1-м из
объектов. Для 2-го объекта применить метод Dispose(), после чего
запустить сборщик мусора. Сообщение об ошибке(захвате) и ход
программы вывести в окне приложения.
2
Создать класс и определить для него 5 объектов. Разместить их в
управляемой куче. Вывести информацию о принадлежности объектов к
поколениям. 2 объекта удалить при помощи метода Dispose(), затем вновь
вывести информацию о поколениях, после чего запустить сборщик мусора.
Вывести на экран информацию о состоянии памяти управляемой кучи.
3
Создать класс и 4 объекта этого класса. Сгенерировать исключения для 2-х
из них по разным критериям. После перехвата исключений запустить
сборщик мусора и вывести на экран информацию об ошибках и
задействованной памяти.
4
Сгенерировать для объекта несколько исключений, одно из которых не
сработает, после чего применить блок finally. Информацию об ошибках и
ходе программы вывести на экран.
- 11 –
5
6
7
8
9
10
Создать несколько объектов класса таким образом, чтобы отреагировать на
возможную нехватку места в управляемой куче. Вывести на экран
информацию об ошибках и принадлежности к поколению объектов.
Создать класс и 5 объектов этого класса. Разместить объекты в
управляемой куче. Сгенерировать исключений для 1-го из них, после чего
вывести на экран информацию о максимальном количестве поколений этих
объектов.
Создать класс и 4 объекта этого класса. Разместить объекты в управляемой
куче. Использовать деструктор C# для удаления 2-х объектов из
оперативной памяти, после чего запустить сборщик мусора. Вывести на
экран состояние памяти, занимаемой в управляемой куче.
Создать класс и 3 объекта этого класса. Для 2-х из них сгенерировать
исключения, используя также блок finally. После этого вывести на экран
сообщение об ошибках и принадлежность к поколениям заданных
объектов.
Создать класс и 5 объектов этого класса. Поместить их в управляемую
кучу. Вывести информацию о поколении объектов. Для 1-го сгенерировать
исключение. Для 2-х объектов использовать метод Dispose(), после чего
запустить сборщик мусора. Вывести также информацию о наличии
ошибок.
Создать класс и 3 объекта этого класса. Поместить их в управляемую кучу.
Для всех объектов применить метод Dispose(). Запустить сборщик мусора,
после чего вывести на экран информацию о поколении объектов. Вывести
на экран состояние памяти управляемой кучи.
2. Ввести программу с клавиатуры с использованием Visual Studio 2005.
3. Отладить программу и запустить на выполнение.
5. СОДЕРЖАНИЕ ОТЧЕТА
В отчете должны быть представлен текст программы, значения вводимых величин
и полученные значения выводимых величин.
6. ВОПРОСЫ ДЛЯ САМОКОНТРОЛЯ
1. Для чего служат исключения ?
2. Каким образом можно сгенерировать исключение ?
3. Что может произойти, если не осуществить перехват исключения ?
4. В чём суть действия блока finally ?
5. Что такое деструктор C# и как он работает ?
6. Каков принцип работы сборщика мусора?
Download