Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

C# для чайников

.pdf
Скачиваний:
183
Добавлен:
27.03.2015
Размер:
15.52 Mб
Скачать

Если вы не знаете, что делать с исключением — лучше не делайте ничего: по­ звольте разобраться с ним вызывающей функции. Но будьте честны сами с со­ бой: не позволяйте исключению уйти только потому, что вы слишком заняты, чтобы писать обработчик.

Регенерация исключения

В ряде случаев метод не в состоянии полностью обработать ошибку, но не хочет пе­ редавать исключение вызывающей функции, не вложив свои "пять копеек" в его обра­ ботку. В таком случае catch-блок может частично выполнить обработку исключения,

азатем передать его дальше (вообще-то, не слишком привлекательная картина). Рассмотрим, например, метод F (), который открывает файл при входе в метод

снамерением закрыть его при выходе из метода. Где-то в середине работы F () вызы­ вается G (). Исключение, сгенерированное в G (), может привести к тому, что у F () не будет шансов закрыть этот файл, который так и останется открытым до полного за­ вершения программы. Идеальным решением было бы включение в F () catch-блока (или блока finally), который бы закрывал все открытые файлы. F() может пере­ дать исключение дальше после того, как закроет все необходимые файлы и выполнит прочие требуемые действия.

"Регенерировать" исключение можно двумя способами. Один из них состоит в гене­ рации второго исключения с дополнительной (или, как минимум, той же) информацией следующим образом:

public void f 1 ()

{

t r y

{

f 2 () ;

}

// Перехват исключения...

catch(MyException me)

{

//... Частичная обработка исключения ...

Console .WriteLine ("Перехват MyException в fl ()")'';

//... Генерация нового исключения для передачи его

//вверх по цепочке вызовов

throw new Exception(

"Исключение, сгенерированное в fl()");

Генерация нового объекта исключения позволяет классу переформулировать сообщение об ошибке, добавив в него дополнительную информацию. Генера­ ция обобщенного объекта Exception вместо специализированного MyEx­ ception обеспечивает гарантированный перехват этого исключения на уров­ нях выше f 1 ().

Генерация нового исключения имеет тот недостаток, что трассировка стека при этом начинается заново, с точки генерации нового исключения. Источник исходной ошибки оказывается потерян, если только fit) не предпримет специальных мер для его сохранения.

(пава 18. Эти исключительные исключения

411

Второй путь состоит во включении в исходный текст команды throw без аргумента что приводит к генерации того же объекта исключения, как показано в следующее фрагменте исходного текста:

public void f l ( )

{

try

{

f 2 () ;

}

// Перехват исключения...

catch(Exception e)

{

// ... Частичная обработка исключения ...

Console.WriteLine("Перехват исключения в f l ( ) " ) ;

//... исходное исключение продолжает свой путь по

//цепочке вызовов

throw;

}

}

Повторная генерация того же объекта имеет свои преимущества и недостатки (ну почему они всегда идут рука об руку?). Регенерация дает возможность промежуточной функции перехватить исключение и освободить или закрыть используемые ресурсы, при этом позволяя объекту исключения донести информацию о месте происшествия до окончательного обработчика этой ошибки. Однако промежуточные функции не могут (или не должны) добавлять какую-либо информацию, модифицируя объект исключения перед его повторной генерацией.

Как реагировать на исключения

Какие у вас имеются варианты при написании catch-блоков? Как объяснялось ранее, вы можете выполнить одно из следующих трех действий:

перехватить исключение; проигнорировать исключение;

частично обработать исключение и повторно его сгенерировать (возможно, с добавлением новой информации) либо просто регенерировать его.

Но какой стратегии необходимо придерживаться при проектировании системы ис­ ключений?

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

Если это возможно — используйте транзакционный подход. Откатите все измене­ ния, которые были сделаны к моменту возникновения ошибки, восстановите фай­ лы в исходное состояние и т.д. Главное правило — не испортить пользовательские данные. Всегда помогайте пользователю в восстановлении, насколько это воз­ можно. Если даже ничего и не получится, то оставит о вас хорошее впечатление...

412

Часть VII. Дополнительные главы

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

Иногда вы не можете сделать ничего. В этом случае вам остается развести ру­ ками, сообщить об этом пользователю и по возможности грациозно завершить ра­ боту программы.

Обычно это сводится к выводу красивого, соболезнующего, но информативного сообщения об ошибке и завершению программы. Если возможно, предложите пользователю варианты его действий по исправлению ситуации.

Следующий пользовательский класс может сохранить дополнительную информацию, что невозможно в процессе применения стандартных объектов Exception или Appli­ cationException:

//MyException - к стандартному классу исключения добавлена

//ссылка на MyClass

public class MyException : ApplicationException

{

private MyClass myobject;

MyException(string sMsg, MyClass mo) : base(sMsg)

{

myobject = mo;

}

//Позволяет внешним классам обращаться к сохраненному

//в исключении классу

public MyClass MyObject{ get {return myobject;}}

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

Перекрытие методов, имеющихся у классов Exception или Applica­ tionException, может предоставить функциям вне BrilliantLi­ brary доступ к новым данным. Рассмотрим класс исключения из следую­ щей демонстрационной программы CustomException.

//CustomException - создание пользовательского исключения,

//которое выводит информацию в более дружественном формате using System;

namespace CustomException

{

 

 

 

public

class CustomException : ApplicationException

 

{

 

 

 

private

MathClass mathobject;

 

private

string sMessage;

 

Глава 18.

Эти исключительные исключения

413

public CustomException(string sMsg, MathClass mo)

{

 

 

 

 

 

mathobject

= mo;

 

 

 

sMessage =

sMsg;

 

 

 

}

 

 

 

 

 

override

public string

Message

 

 

{

 

 

 

 

 

get{return

String.Format("Сообщение

< { o

} > ,

 

 

 

Объект {l}",

 

 

 

 

sMessage,

 

 

 

 

 

mathobj ect.ToString());}

override

public string

ToString()

 

 

{

 

 

 

 

 

string

s =

Message;

 

 

 

s += "\пИсключение сгенерировано в

";

 

s += TargetSite.ToString(); // Информация

о методе,

 

 

 

// сгенерировавшем

исключение

return

s;

 

 

 

 

}

}

// MathClass - набор созданных мною математических функций public class MathClass

{

private

int nValueOfObject;

private

string sObjectDescription;

public

MathClass(string sDescription, int nValue)

{

 

 

 

nValueOfObject

= nValue;

sObjectDescription =

sDescription;

public

int Value

{get

{return nValueOfObject;}}

//Message - вывод сообщения со значением

//присоединенного объекта MathClass public string Message

{

get

{

return String.Format("({0} = {l})",

sObj ectDescription, nValueOfObject);

}

}

//ToString - расширение нашего пользовательского

//свойства Message с использованием Message из базового

//класса исключения

override

public string

ToString()

{

 

 

string s = Message +

"\n";

s +- base.ToString();

return

s;

 

}

// Вычисление обратного значения 1/x

public double

Inverse()

414

Часть VII. Дополнительные глава

{

if (nValueOfObject == 0)

{

throw new CustomException("Нельзя делить на 0", this);

}

return 1.0 / (double)nValueOfObject;

public class Program

{

public static void Main(string[] args)

try

{

// take the inverse of 0

MathClass mathObject = new MathClass("Value", 0 ) ; Console.WriteLine("Обратное к d.Value равно {О}",

mathObj ect.Inverse() ) ;

catch(Exception e)

{

Console.WriteLine(

"\пНеизвестная фатальная ошибка:\n{0}",

e.ToString());

}

// Ожидаем подтверждения пользователя Console.WriteLine("Нажмите <Enter> для " +

"завершения программы.. . ") ;

Console.Read();

}

Класс CustomException несложен. Он хранит сообщение и объект, как это делал класс MyException ранее. Однако вместо предоставления новых методов для обраще­ ния к этим элементам данных он перекрывает существующее свойство Message, кото­ рое возвращает сообщение об ошибке, содержащееся в исключении, и метод ToString ( ) , возвращающий сообщение и трассировку стека.

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

Функция Main () демонстрационной программы начинает с создания объекта MathClass со значением 0, а затем пытается вычислить обратную к нему величину. Не знаю, как вам, а мне не приходилось видеть разумные результаты деления на 0, так что если моя функция вдруг сделает это, я отнесусь к происшедшему с явным недоверием.

На самом деле процессоры Intel возвращают значение 1.0/0.0: бесконечность. Имеется ряд специальных значений с плавающей точкой, используемых вместо генерации исключений в языках, которые не поддерживают их. Эти специаль­ ные значения включают положительную и отрицательную бесконечности и по­ ложительное и отрицательное NaN (Not_a_Number, не число).

(пава 18. Эти исключительные исключения

415

В нормальных условиях метод

Inverse () возвращает корректное значение. При

передаче ему нуля он генерирует

исключение CustomException, передавая ему стро­

ку пояснения вместе с вызвавшим

исключение объектом.

Функция Main () перехватывает исключение и выводит короткое сообщение, поясняющее суть происшедшего. "Неизвестная фатальная ошибка", вероятно, означает, та программа "закрывает лавочку и уходит на отдых". Но функция Main () дает исключе­ нию шанс пояснить, что же все-таки произошло, вызывая его метод ToString ().

Визитка класса: метод ToString ()

Все классы наследуют один общий базовый класс с именем Object. Об этом уже го­ ворилось в главе 14, "Интерфейсы и структуры". Здесь, однако, стоит упомянуть о ме­ тоде ToString () в составе этого класса. Метод предназначен для преобразования содержимого класса в строку. Идея заключается в том, что каждый класс должен пе­ рекрывать метод ToString ( ) , чтобы осуществить вывод значащей информации. В первых главах был использован метод GetString ( ) , чтобы не касаться в них во­ проса наследования; однако принцип остается тем же. Например, корректный метод Student. ToString () может выводить имя и идентификатор студента.

Большинство функций — даже встроенных в библиотеку С # — применяют метод ToString () для вывода объектов. Таким образом, перекрытие ToString () имеет очень полезное побочное действие, заключающееся в том, что каждый объект выво­ дится в своем собственном формате, безотносительно к тому, кем именно он выведен.

Поскольку объект исключения в этом случае на самом деле принадлежит типу Cus­ tomException, управление передается CustomException. ToString ().

Метод Message () представляет собой виртуальный метод класса Excep­ tion, так что его можно перекрывать, но пользовательское исключение долж­ но наследовать его без перекрытия.

Метод Message О позволяет объекту MathClass выводить информацию о самом себе с использованием метода ToString ( ) . Метод MathClass.ToString() воз­ вращает строку, в которой содержится описание и значение объекта.

Не следует брать на себя больше того, что имеете. Используйте метод объекта ToString () для создания строковой версии объекта, не пытаясь влезть в сам объект и получить его значения. В общем случае нужно полагаться на откры­ тый интерфейс — открытые члены, — а не на знания о внутреннем устройстве объекта. Оно инкапсулировано (по крайней мере должно быть инкапсулирова­ но) и может измениться в новых версиях.

Вывод демонстрационной программы CustomException имеет следующий вид:

Неизвестная фатальная ошибка:

Сообщение <Нельзя делить на 0>, Объект (Value = 0) CustomException.MathClass

Исключение сгенерировано в Double Inverse() Нажмите <Enter> для завершения программы...

416

Часть VII. Дополнительные глава

И последнее: сообщение "Неизвестная фатальная ошибка:" поступает от

Main ( ) . Строка "Сообщение <Нельзя делить на 0>, Объект <~~>" посту­ пает от CustomException. Часть Value = 0 предоставляет объект MathClass. Последняя строка, Исключение сгенерировано в Double Inverse ( ) , при­ надлежит CustomException. Это нельзя назвать иначе, как исключительным со­ трудничеством.

Глава 18. Эти исключительные исключения

417

Глава 19

Работа с файлами и библиотеками

>Работа с несколькими исходными файлами в одной программе У Сборки и пространства имен

>Библиотеки классов

>Чтение и запись файлов данных

оступ к файлам в С# может иметь два различных значения. Наиболее очевид­ ное — это хранение и считывание данных с диска. О том, как осуществляется ввод-вывод данных с диска, вы узнаете из этой главы. Второе значение связано

с тем, каким образом исходный текст С# группируется в исходные файлы.

Функции позволяют разделить длинную строку исходного текста на отдельные моду­ ли, которые можно легче сопровождать и поддерживать. Классы дают возможность группировать данные и функции для дальнейшего снижения сложности программы — программы достаточно сложны, а людям свойственно ошибаться, так что нужно пользо­ ваться любой возможностью упрощения, которая может помочь избежать ошибок.

С# обеспечивает еще один уровень группировки: он позволяет сгруппировать подоб­ ные классы в отдельную библиотеку. Помимо написания собственных библиотек, вы можете использовать в ваших программах и чужие библиотеки. Такие программы со­ держат множество модулей, называемых сборками (assemblies). О них также будет рас­ сказано в данной главе. Кроме того, описанное в главе 11, "Классы", управление досту­ пом на самом деле несколько сложнее в связи с применением пространств имен — еще одного способа группирования похожих классов, которое заодно позволяет избежать дублирования имен в двух частях программы. В этой главе речь пойдет и о них.

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

Рассмотрим систему продажи авиабилетов. У вас должен быть один интерфейс для заказа билетов по телефону, другой — для тех, кто заказывает билет по Интернету, должна быть часть программы, отвечающая за управление базой данных билетов, дабы не продавать один и тот же билет несколько раз, еще одна часть должна следить за стой-

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

Размещение всех составляющих программу классов в одном исходном файле Pro­ gram, с s быстро становится непрактичным. Оно даже более неприемлемо, чем разде имущества, которого добилась моя бывшая жена, по следующим причинам.

У вас возникнут проблемы при поддержке классов. Единый исходный фа очень трудно поддается пониманию. Гораздо проще разбить его на отдельные мо­ дули, например ResAgentlnterf асе . cs, GateAgentlnterf асе . cs, Res-j

Agent.cs, GateAgent.cs, Fare.cs и Aircraft.cs.

Работа над большими программами обычно ведется группами программи­ стов. Два программиста не в состоянии редактировать одновременно один и та же файл — каждому требуется его собственный исходный файл (или файлы).; У вас может быть 20 или 30 программистов, одновременно работающих над од­ ним большим проектом. Один файл ограничит работу каждого из 24 программи­ стов над проектом всего одним часом в сутки, но стоит разбить программу на 24 файла, как становится возможным (хотя и сложным) заставить всех программи­ стов трудиться круглые сутки. Разбейте программу так, чтобы каждый класс со­ держался в отдельном файле, и ваша группа заработает как слаженный оркестр.

Компиляция больших файлов занимает слишком много времени. В результа­ те босс начнет нервничать и выяснять, почему это вы так долго пьете кофе вместо того, чтобы стучать по клавишам?

Какой смысл перестраивать всю программу, когда кто-то из программистов изме­ нил пару строк кода? Visual Studio 2005 может перекомпилировать только изме­ ненный файл и собрать программу из уже готовых объектных файлов.

По всем этим причинам программисты на С# предпочитают разделять программу на отдельные исходные файлы . CS, которые компилируются и собираются вместе в единый выполнимый . ЕХЕ-файл.

Файл проекта содержит инструкции о том, какие файлы входят в проект и как они должны быть скомбинированы друг с другом.

Можно объединить файлы проектов для генерации комбинаций программ, которые зависят от одних и тех же пользовательских классов. Например, вы можете захотеть объ­ единить программу записи с соответствующей программой чтения. Тогда, если изменя­ ется одна из них, вторая перестраивается автоматически. Один проект может описывать программу записи, второй — программу чтения. Набор файлов проектов известен под

названием решение

(solution). (Далее в главе будут рассматриваться две такие програм­

м ы — FileRead и

FileWrite, которые можно было бы объединить в одно решение,

но это так и не было сделано.)

Программисты на Visual С# используют Visual Studio Solution Explorer для объ­ единения нескольких исходных файлов С# в проекты в среде Visual Studio 2005. Solution Explorer будет описан в главе 21, "Использование интерфейса Visual Studio".

420

Часть VII. Дополнительные главы

В Visual Studio, а также в C#, Visual Basic .NET и прочих языках .NET один проект соответствует одному скомпилированному модулю — в .NET он носит имя сборка.

С# может создавать два основных типа сборок — выполнимые файлы (с расширени­ ем .ЕХЕ) и библиотеки классов (.DLL). Выполнимые файлы представляют собой про­ граммы сами по себе и используют код поддержки из библиотек. Во всей этой книге созда­ вались исключительно выполнимые файлы. Что касается библиотек классов, то опять же все программы в книге их используют. Например, пространство имен System— место размещения таких классов, как String, Console, Exception, Math и Object— су­ ществует как набор библиотечных сборок. Каждой программе требуются классы System.

Библиотеки не являются самостоятельными выполнимыми программами.

Библиотека классов состоит из одного или нескольких классов, обычно работающих вместе тем или иным способом. Зачастую классы в библиотеках находятся в своем соб­ ственном пространстве имен (namespace). (О них речь пойдет в следующем разделе.) Вы можете построить библиотеку математических подпрограмм, библиотеку для работы со строками, библиотеку классов-фабрик и т.д.

Небольшие программы обычно состоят из одной сборки programName.ехе. Одна­ ко часто создаются решения, состоящие из нескольких отдельных (но связанных) проек­ тов, как упоминалось в предыдущем разделе. Каждый из них компилируется в отдельную сборку. В решении вы можете объединять и . ЕХЕ-, и . DLL-файлы, что является обычной практикой ДЛЯ больших программ. Когда ВЫ строите многопроектное решение, сборки работают совместно, обеспечивая функциональность приложения в целом.

Если решение содержит более одного . ЕХЕ-проекта, вы должны указать Visual Studio, какой проект является начальным (startup project). Именно он будет запус­ каться при выборе команды меню Debug^Start Debugging (F5) or Debugs Start Without Debugging (<Ctrl+F5>). Для указания начального проекта щелкни­ те на нем правой кнопкой мыши в окне Solution Explorer и выберите в раскры­ вающемся меню команду Set as Startup Project. Имя начального проекта в окне Solution Explorer выделяется полужирным шрифтом. О Solution Explorer речь пойдет в главе 21, "Использование интерфейса Visual Studio".

Большие программы обычно разделяют свои компоненты на один выполнимый файл и несколько библиотек. Например, весь код, связанный с заказом билетов в рассматривав­ шемся ранее приложении, может находиться в одной библиотеке, работа с Интернетом — в другой, а управление базами данных — в третьей. Когда такая программа устанавливает­ ся на компьютер пользователя, процесс инсталляции включает копирование ряда файлов в соответствующие места на диске компьютера, причем многие из них являются .DLL- файлами, или просто "DLL" на сленге программистов (DLL означает dynamic link library (динамически компонуемые библиотеки) — код, который загружается в память тогда, ко­ гда в нем возникает необходимость при запуске используемой программы).

taa 19. Работа с файлами и библиотеками

421

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]