Kniga-Online.club
» » » » Эндрю Хант - Программист-прагматик. Путь от подмастерья к мастеру

Эндрю Хант - Программист-прагматик. Путь от подмастерья к мастеру

Читать бесплатно Эндрю Хант - Программист-прагматик. Путь от подмастерья к мастеру. Жанр: Программирование издательство -, год 2004. Так же читаем полные версии (весь текст) онлайн без регистрации и SMS на сайте kniga-online.club или прочесть краткое содержание, предисловие (аннотацию), описание и ознакомиться с отзывами (комментариями) о произведении.
Перейти на страницу:

Подсказка 35: Доводите до конца то, что начинаете

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

void readCustomer(const char *fName, Customer *cRec) {

cFiie = fopen(fName, "r+");

fread(cRec, sizeof(*cRec), 1, cFile);

}

void writeCustomer(Customer *cRec) {

rewind(cFile);

fwrite(cRec, sizeof(*cRec), 1; cFile);

fclose(cFile);

}

void updateCustomer(const char *fName, double newBalance) {

Customer cRec;

readCustomer(fName, &cRec);

cRec.balance = newBalance;

writeCustomer(&cRec);

}

На первый взгляд, подпрограмма updateCustomer выглядит довольно прилично. Похоже, что она реализует нужную нам логику – считывает запись, обновляет баланс и осуществляет запись обратно в файл. Однако, за внешним приличием и скрывается главная проблема. Подпрограммы readCustomer и writeCustomer тесно связаны между собой [27] – они совместно используют глобальную переменную cFile. Подпрограмма readCustomer открывает файл и сохраняет указатель файла в переменной cFile, а подпрограмма writeCustomer использует сохраненный указатель для закрытия файла по окончании работы. Эта глобальная переменная даже не появляется в подпрограмме updateCustomer.

Почему эту программу можно считать неудачной? Представим несчастную даму-программистку из службы сопровождения, которой сказали, что спецификация изменилась – баланс должен обновляться только в том случае, если его новое значение неотрицательное. Дама открывает файл с исходным текстом и изменяет подпрограмму updateCustomer:

void updateCustomer(const char *fName, double newBalance) {

Customer cRec;

readCustomer(fName, &cRec);

if (newBalance >= 0.0) {

   cRec.balance = newBalance;

   writeCustomer(&cRec);

  }

}

Во время испытания все выглядит замечательно. Однако когда программа вводится в эксплуатацию, она «падает» через несколько часов, выдавая сообщение "Слишком много открытых файлов". Поскольку подпрограмма writeBalance в ряде случаев не вызывается, то и файл не закрывается.

У этой проблемы есть весьма неудачное решение – использовать специальный оператор выбора в подпрограмме updateCustomer:

void updateCustomer(const char *fName, double newBalance) {

Customer cRec;

readCustomer(fName, &cRec);

if (newBalance»= 0.0) {

  cRec.balance = newBalance;

  writeCustomer(&cRec);

}

else

fclose(cFile);

}

Это устраняет проблему – файл закроется независимо от нового значения баланса, но эта процедура означает, что теперь связанными оказываются три подпрограммы (через глобальную переменную cFile). Мы попадаем в ловушку, и если мы продолжаем действовать в том же духе, то все полетит под откос.

Подсказка "Доводите до конца то, что начинаете" говорит нам о том, что в идеале подпрограмма, которая назначает ресурс, обязана его и освобождать. Мы можем применить ее в данном случае, осуществляя небольшую реорганизацию программы:

void readCustomer(FILE *cFile, Customer *cRec) {

fread(cRec, sizeof(*cRec), 1, cFile);

}

void writeCustomer(FILE *cFile, Customer *cRec) {

rewind(cFile);

fwrite(cRec, sizeof(*cRec), 1, cFile);

}

void updateCustomer(const char *fName, double newBalance) {

FILE *cFile;

Customer cRec;

cFile = fopen(fName, "r+"); // ->>>

readCustomer(cFile, &cRec); //

if (newBalance >= 0.0) {  //

  cRec.balance = newBalance; //

  writeCustomer(cFile, &cRec); //

  } //

 Fclose(cFile);   // <<<-

}

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

Вложенное роспределение ресурса

Базовая схема распределения ресурсов может быть распространена на подпрограммы, которым одновременно необходимо более одного ресурса. Поэтому есть еще два предложения:

1. Освобождайте ресурсы в последовательности, обратной той, в которой происходило их распределение. При этом можно избежать появления «осиротевших» ресурсов, если один из них содержит ссылки на другой.

2. При распределении одного и того же набора ресурсов в различных местах программы необходимо осуществлять эту операцию в одном и том же порядке. Это уменьшает вероятность взаимоблокировки. (Если процесс А требует resource1 и собирается затребовать resource2, тогда как процесс В затребовал resource2 и пытается заполучить resource1, то два процесса окажутся в состоянии вечного ожидания.)

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

Объекты и исключения

Равновесие между распределениями ресурсов и их освобождениями напоминает о равновесии конструктора и деструктора класса. Класс представляет ресурс, конструктор создает конкретный для этого типа ресурса объект, а деструктор удаляет его из вашей области действия.

Если вы программируете на объектно-ориентированном языке, то упаковка ресурсов в классы может принести пользу. Всякий раз, когда вам необходим конкретный тип ресурса, вы создаете экземпляр объекта указанного класса. Если объект выходит из области действия или повторно запрашивается сборщиком мусора, то деструктор объекта освобождает инкапсулированный ресурс.

Этот подход обладает определенными преимуществами при работе с языками программирования типа С++, где исключения могут входить в противоречие с освобождением ресурсов.

Балансировка и исключения

Языки, поддерживающие исключения, могут сделать процедуру освобождения ресурса нетривиальной. Как удостовериться, что все ресурсы, назначенные до возбуждения исключения, освобождены надлежащим образом? В некоторой степени ответ зависит от языка программирования.

Балансировка ресурсов в исключениях языка С++

Язык С++ поддерживает механизм исключений типа try…catch. К сожалению, это означает, что всегда существует по крайней мере два возможных варианта выхода из подпрограммы, которая перехватывает, а затем повторно возбуждает исключение:

void doSomething(void) {

  Node *n = new Node;

  try {

   // do something

  }

  catch (…) {

    delete n;

    thow;

  }

delete n;

}

Заметим, что созданный нами узел освобождается дважды – один раз во время нормального выхода из подпрограммы, а второй раз в обработчике исключений. Это явное нарушение принципа DRY и проблема в сопровождении, которая может возникнуть в любой момент.

Однако в наших интересах воспользоваться семантикой языка С++. Локальные объекты автоматически разрушаются при выходе из блока, в котором они находятся. Это дает нам несколько вариантов. Если обстоятельства позволяют, можно поменять n: оно обозначает не указатель, а реальный объект Node в стеке:

void doSomething1(void) {

Node n;

try {

// делаем что-либо

}

catch (…) {

throw;

 }

}

В этом случае мы используем С++ для автоматического разрушения объекта Node независимо от того, возбуждено исключение или нет.

В случае, если замена указателя на объект невозможна, тот же самый эффект достигается при инкапсулировании ресурса (речь идет об указателе Node) в пределах другого класса.

// Класс оболочки для ресурсов Node

class NodeResource {

  Node *n;

public:

  NodeResource() {n = new Node;}

 ~NodeResource() {delete n;}

 Node *operator ->() {return n;}

};

void doSomething2(void) {

NodeResource n;

try {

// do something

}

catch (…) {

   throw;

 }

}

Теперь класс-оболочка NodeResource выступает гарантом того, что при разрушении его объектов происходит и разрушение соответствующих узлов. Для удобства класс оболочка предоставляет оператор разыменования – », с тем чтобы пользователи могли обращаться к полям в инкапсулированном объекте Node напрямую.

Поскольку эта методика столь полезна, в стандартной библиотеке С++ имеется шаблонный класс autOjDtr, обеспечивающий автоматические оболочки для динамически размещаемых объектов.

void doSomething3(void) {

   auto_ptr <Node> р (new Node);

// Обращение к узлу Node как р-»…

// В конце узел автоматически удаляется

}

Балансировка ресурсов в языке Java

В отличие от C++ язык Java реализует «ленивую» форму автоматического разрушения объекта. Объекты, ссылки на которые отсутствуют, считаются кандидатами на попадание в «мусор», и их метод finalize будет вызываться в любой момент, когда процедура сборки мусора будет претендовать на эти объекты. Представляя собой удобство для разработчиков, которым больше не приходится жаловаться на утечки памяти, в то же время он усложняет реализацию процедуры очистки ресурсов по схеме С + +. К счастью, разработчики языка Java глубокомысленно ввели компенсирующую языковую функцию – предложение finally. Если блок try содержит предложение finally, то часть программы, относящаяся к этому предложению, гарантированно исполняется только в том случае, если исполняется любая инструкция в блоке try. Неважно, возбуждается при этом исключение или нет (даже при выполнении оператора return программой в блоке try) – программа, относящаяся к предложению finally, будет выполнена. Это означает, что использование ресурса может быть сбалансировано с помощью программы типа:

Перейти на страницу:

Эндрю Хант читать все книги автора по порядку

Эндрю Хант - все книги автора в одном месте читать по порядку полные версии на сайте онлайн библиотеки kniga-online.club.


Программист-прагматик. Путь от подмастерья к мастеру отзывы

Отзывы читателей о книге Программист-прагматик. Путь от подмастерья к мастеру, автор: Эндрю Хант. Читайте комментарии и мнения людей о произведении.


Уважаемые читатели и просто посетители нашей библиотеки! Просим Вас придерживаться определенных правил при комментировании литературных произведений.

  • 1. Просьба отказаться от дискриминационных высказываний. Мы защищаем право наших читателей свободно выражать свою точку зрения. Вместе с тем мы не терпим агрессии. На сайте запрещено оставлять комментарий, который содержит унизительные высказывания или призывы к насилию по отношению к отдельным лицам или группам людей на основании их расы, этнического происхождения, вероисповедания, недееспособности, пола, возраста, статуса ветерана, касты или сексуальной ориентации.
  • 2. Просьба отказаться от оскорблений, угроз и запугиваний.
  • 3. Просьба отказаться от нецензурной лексики.
  • 4. Просьба вести себя максимально корректно как по отношению к авторам, так и по отношению к другим читателям и их комментариям.

Надеемся на Ваше понимание и благоразумие. С уважением, администратор kniga-online.


Прокомментировать
Подтвердите что вы не робот:*
Подтвердите что вы не робот:*