Лекции (1129116), страница 25
Текст из файла (страница 25)
PVF * offset_handler;
public:
SUE(PVF * new_h) {old_handler=set_unexpected(new_h) };
~SUE( ) {set_unexpected(old_handler);}
};
В данном случае деструктор восстанавливает старый обработчик исключительных ситуаций.
Надо, правда, отметить, что вся эта процедура сводится к красивому умиранию. Кроме, посмертного дампа практически невозможно выдать что-то осмысленное.
{ SUE S(MyHand);
try { …
} catch ( );
};
Неперехваченные исключения – очень большая проблема для программиста на C++.
Довольно стандартная ситуация, когда есть один catch( ) в main, который ловит все исключения и выдает неосмысленные сообщения. Поэтому надо писать аккуратно и исключения и ловушки.
По-настоящему надежный язык должен не только вводить механизм исключений, он должен еще заставлять программиста писать обработчики исключений надлежащим образом. Совершенно недостаточно спланировать потенциальные ошибочные ситуации, самое главное – правильно написать обработчики ошибок, которые будут минимизировать ущерб. Для этого обработчики должны оказываться как можно ближе к месту возникновения ошибки. Следовательно, при планировании обработчика следует обратить внимание, какие исключительные ситуации возникают.
Обычно во всех библиотеках после описания классов и методов пишутся те исключения, которые генерирует соответствующий класс и метод. После анализа этого списка следует сделать вывод – где на какое исключение надо реагировать.
В C++ есть такая возможность. Общий синтаксис позволяет написать:
void f( ) throw (список типов)
{
…
}
Это следует читать так: функция f ( ) может возбуждать исключения, относящиеся к указанному списку типов. Все остальные исключения либо обрабатываются внутри f(), либо являются неожиданными. Если через эту функцию проходит неожиданное исключение, то именно в этот момент работает функция set_unexpected. Как правило, это также посмертный дамп, но уже более локализованный к месту возникновения ошибки.
Плохо то, что C++ дает такую возможность, но не заставляет программиста ее использовать.
Delphi проблему игнорирует, он не претендует на роль надежного языка.
А вот Java претендует. В нем эта возможность сделана обязательной.
В Java исключения делятся на два вида:
- пользовательские
- системные
на системные исключения программист реагировать не обязан, а вот на пользовательские – обязан. Если где-то есть:
try { …
throwUserError;
} catch (UserError) {…}; // обязательно должна быть ловушка
если ловушки нет внутри, то это должно быть объявлено в заголовке функции:
void f( ) throws (список типов)
То есть для любой ошибки мы должны либо ее отлавливать, либо объявлять, что это должно происходить где-то выше.
Корректность этого требования можно проверить уже при компиляции. Если исключение может распространиться выше, а там не отлавливается, то компилятор выдаст ошибку.
Таким образом, дойти до самого верхнего уровни может исключение, которому мы разрешили это сделать, либо системное исключение.
Язык Java заставляет писать надежные программы (либо написать обработчик, либо расписаться в своей некомпетентности).
Часть II. Объектно-ориентированные ЯП.
Прежде чем говорить об ОО ЯП следует поговорить о том, что есть ООП вообще и т.д. Но это тема совершенно мертвая, так как об этом спорят достаточно долго и много. Однако общие принципы устоялись. ООП включает в себя три концепции:
-
Инкапсуляция
-
Наследование
-
Полиморфизм
Что такое объекто-ориентированный дизайн – можно еще спорить. Но то, какими свойствами должны обладать ОО ЯП – вещь вполне определенная:
-
Явным образом должно быть сформулировано понятие объекта и класса. Гарри Буч, известный специалист по объетно-ориентированному программированию приводит в своих трудах примеры объектных программ на Ada, хотя этот язык и не является ОО. Такие языки, где есть понятия объекта и класса (или они как-то выражаются) называют просто объектными. Например, на чистом С также можно писать объектные программы (например, XToolkit – одна из самых объектно-ориентированных систем, написана на чистом Керниган-Ритчи С). Понятие объекта включает в себя:
-
инкапсуляцию
-
абстрагирования (абстрактные типы данных)
-
параметризацию
-
Наследование. Здесь ОО ЯП отходят от традиционных ЯП, где считается, что объекты не могут пересекаться между собой. В ОО ЯП вводится механизм выведения типа из другого типа. (Терминология здесь разная: если из T выводится T1, то в C++T называется базовым, T1 – выведенным; в SmallTalk – суперкласс и класс. ) Вирт, например, говорит, что вводится механизм не наследования, а обогащения или расширения типа. Собственно, Oberon – урезанная Modula-2, в которую введена возможность обогащения типа.
Полиморфизм. Настоящие ОО ЯП обладают полиморфными операциями. Это операции, которые применимы к различным типам параметрам. Мы обсуждали статический полиморфизм – перекрытие функций, когда одному имени соответствует несколько профилей, и компилятор делает выбор между функциями статически на стадии компиляции. Но перекрытия не хватает в ОО. Необходим динамический полиморфизм, когда выбор о применении той или иной функции можно будет делать динамически. В C++ для этого существует аппарат виртуальных функций. В Java все функции полиморфны (виртуальны).
Если язык обладает этими свойствами, то он является объектно-ориентированным.
Концепции объекта и класса мы подробно обсуждали на протяжении всего предыдущего времени, поэтому будет правильным сразу перейти к обсуждению следующего пункта:
Глава I. Наследование в ЯП.
Сейчас мы будем говорить о единичном наследовании – у каждого типа может быть не более одного родителя.
Самый простой способ реализован в Oberon:
TYPE T=RECORD
X: INTEGER;
Y: REAL;
END;
Мы выводим из T T1 таким образом:
TYPE T1=RECORD(T)
Z: CHAR;
END;
Заметим, что в Oberon структура данных принципиально отделяется от операций. Они объединяются с помощью модуля.
T1 обладает, как полями X,Y, так и Z. Более того, все операции над T автоматически переносятся и на T1:
PROCEDURE P(VAR X:T);
Если есть:
VAR X:T, X1:T1;
то можно писать:
X1.Z; X1.X; X1.Y;
и
P(X); P(X1);
Это безопасно, так как процедура P может работать с полями X,Y, а значит она может работать с любым наследником, так как в нем есть такие же поля.
Что дает наследование? Вспомним Stack. Мы сможем без механизма статической параметризации реализовать различные стеки:
TYPE StackObj= RECORD END;
- это пустой тип данных (имеем право)
А теперь мы можем создать стек, где элементами стека будут объекты StackObj (пустой тип):
TYPE STACK = RECORD
TOP: INTEGER;
BODY: ARRAY 50 OF PStackObj;
END;
PStackObj = POINTER TO StackObj;
PROCEDURE Push (VAR s: STACK; x: PStackObj);
PROCEDURE Pop (VAR s: STACK; x: PStackObj);
А вот теперь мы создадим наследников StackObj, обладающих реальными качествами, в частности стек для хранения целых чисел:
TYPE IntStackObj = RECORD (StackObj)
I: INTEGER;
END;
PIntStockObj = POINTER TO IntStackObj;
Для этого наследника также действенны операции Push и Pop.
Следует отметить, что свойства наследования также распространяются и на указатели родителей<->наследников, что видно из приведенного примера. Заметим, что вообще обычно работа идет в терминах указаетелей.
Далее, можно поступать так:
VAR P: PIntStackObj;
P:=NEW (StackObj);
P.I=5;
Push(S,P);
…
Pop(S,P);
Поскольку P наследует StackObj, то данный кусок будет работать корректно. За исключением одного момента – что вернет функция Pop? В данном случае Int, а если у нас не тольк IntStackObj определен, а еще и:
TYPE IntStackObj= RECORD (StackObj)
C: CHAR;
END;
PCharStackObj=…;
Этот тип мы также можем класть в стек. В результате у нас появляется гетерогенный контейнер, способный хранить любые объекты. Но тут возникает беда, предположим у нас есть:
P1: PCharStackObj;
P2: PIntStackObj;
Push(S,P1); // здесь символ
Push(S,P2); // здесь число
Pop(S,P1); // что мы получим здесь?
Ведь размеры символов и чисел в памяти нам неизвестны, это еще зависит и от платформы (на Intel платформе байты хранятся в обратном порядке). В любом случае мы не получим то, что хотим. Конечно, это плохо. Но существуют средства динамической идетнификации типа, позволяющие отловить подобные вещи:
В Oberon введено два понятия – страж типа и динамическая проверка типа. Динамическая проверка, это выражение вида:
t is T
где t – некоторе выражение, а T – тип данных. У каждого выражения есть некоторый статический тип, на него мы можем проверить переменную. Это выражение вернет «правду», если t имеет тип t или производный от него.
VAR P: PStackObj; // PStackObj – статический тип
Pop (S,P);
Таким образом объект P обладает статическим типом PStackObj, а динамическим его типом может являться PIntStackObj или PCharStackObj, это мы можем проверить:
IF P IS PIntStackObj THEN
<работать, как с целым>
ELSEIF
P IS PCharStackObj THEN
<работать, как с символьным>
Следующий тип данных:
RECORD END;
в некотором смысле не является пустым, он указывает на некоторую область памяти, по которой мы можем идентифицировать тип.
Как же теперь работать с P, ведь он - базовый. Для этого существует страж типа.
t(T1) – страж типа
- он говорит программе трактовать t, как переменную T1 типа. Таким образом, мы можем написать:
P(PIntStackObj).I
…
P(PCharStackObj).C
За счет того, что компилятор вставляет некоторый кусочек проверки типа, у нас получается вполне корректный код.
Лекция 21
Мы рассмотрели наследование в языке Оберон. Чем же объектно-ориентированные языки, с точки зрения типовой структуры, отличаются от традиционных. Для этого нужно вспомнить концепцию уникальности типа (КУТ), которая была в традиционных языках программирования и состояла из четырех компонентов:
1. Каждый объект данных имеет ровно один тип.
Т.е. множество типов разбивается на непересекающиеся множества. В объектно-ориентированных языках каждому объекту данных соответствует некий статический тип (т.е. характеристики этого типа данных известны в период компиляции). Но сами типы не разбиваются на непересекающиеся множества, а между ними вводится несимметричное отношение наследования. Причем это отношение транзитивно. При этом все объекты производного типа обладают всеми свойствами (структуры данных + набор операций) базового типа. Поэтому такие объекты фактически принадлежат сразу к нескольким типам – к своему непосредственному типу и всем его базовым типам. Поэтому уже нельзя говорить, что каждому объекту данных соответствует только один статический тип.
Если не использовать наследование, то мы получаем концепцию традиционных языков программирования. Т.е. наследование расширяет эту концепцию, но не сужает ее.
Однако некоторые объекты данных могут менять свой тип в процессе выполнения программы, следовательно, у объекта появляется еще и динамический тип. Какие объекты могут менять свой тип? Тут различные объектно-ориентированные языки различаются. Тем не менее, во всех объектно-ориентированных языках некоторые объекты могут менять свой тип. При этом, область изменчивости этого типа ограничена статическим типом и всеми производными типами от данного статического типа. Причем в общем случае нет возможности отследить, какого рода динамический тип будет у данного объекта.
2. Типы эквивалентны тогда и только тогда, когда их имена совпадают.
Мы говорили, что есть еще отношение эквивалентности типов в таких языках, как Ада, Паскаль и МОДУЛА-2, когда вводятся синонимы типов, но это не ограничивает общности суждения. В объектно-ориентированных языках это правило безусловно выполняется для статических типов. Но кроме эквивалентности типов, возникает еще понятие совместимость типов. Два типа являются совместимыми тогда и только тогда, когда один из них является производным (прямо или косвенно) от другого. Заметим, что типы, находящиеся на разных ветвях иерархии совместимыми не являются. Что такое совместимость, мы разберем несколько позже.
3. Каждый тип данных характеризуется набором данных и множеством операций.
Это правило сохраняется для объектно-ориентированных языков. Но из-за наследования, при определении некоторого статического типа, мы определяем некий набор данных и набор операций, который характерен именно для этого статического типа. Но при этом, каждый статический тип еще наследует все атрибуты базовых типов.
4. Различные типы не совместимы по присваиванию и передаче параметров.
Это утверждение и говорит о том, что типы никак не пересекаются. Конечно же, существуют дырки в системе типов, которые реализуются с помощью стандартных преобразований через понятие адреса. Но использование этих дырок потенциально небезопасно.
Что означает совместимость типов? Пусть есть два совместимых типа Т и Т1, и пусть Т1 является производным типом от Т. И пусть есть объекты этих двух типов:
T x;