Гради Буч - Объектно-ориентированный анализ и проектирование с примерами приложений на С++ (1158635), страница 17
Текст из файла (страница 17)
В сильно типизированных языках нарушениесогласования типов может быть обнаружено во время трансляции программы. С другой стороны, вSmalltalk типов нет: во время исполнения любое сообщение можно послать любому объекту, иесли класс объекта (или его надкласс) не понимает сообщение, то генерируется сообщение обошибке. Нарушение согласования типов может не обнаружиться во время трансляции и обычнопроявляется как ошибка исполнения. C++ тяготеет к сильной типизации, но в этом языке правилатипизации можно игнорировать или подавить полностью.Рассмотрим абстракцию различных типов емкостей, которые могут использоваться внашей теплице.
Вероятно, в ней есть емкости для воды и для минеральных удобрений; хотяпервые предназначены для жидкостей, а вторые для сыпучих веществ, они имеют достаточномного общего, чтобы устроить иерархию классов. Начнем с типов.// Число, обозначающее уровень от 0 до 100 процентов8Тип и класс не вполне одно и то же; в некоторых языках их различают. Например, ранние версии языкаTrellis/Owl разрешали объекту иметь и класс, и тип. Даже в Smalltalk объекты классов SmallInteger,LargeNegativeInteger, LargePositiveInteger ОТНОСЯТСЯ К одному типу Integer, хотя и к разным классам[69]. Большинству смертных различать типы и классы просто противно и бесполезно.
Достаточно сказать,что класс реализует понятие типа.typedef float Level;Операторы typedef в C++ не вводят новых типов. В частности, и Level и Concentration — насамом деле другие названия для float, и их можно свободно смешивать в вычислениях. В этомсмысле C++ имеет слабую типизацию: значения примитивных типов, таких, как int или floatнеразличимы в пределах данного типа.
Напротив, Ada и Object Pascal предоставляют сильнуютипизацию для примитивных типов. В Ada можно объявить самостоятельным типом интервалзначений или подмножество с ограниченной точностью.Строгая типизация предотвращает смешивание абстракцийПостроим теперь иерархию классов для емкостей:class StorageTank {public:StorageTank();virtual ~StorageTank();virtual void fill();virtual void startDraining() ;virtual void stopDraining();Boolean isEmpty() const;Level level() const;protected:…};class WaterTank : public StorageTank {public:WaterTank();virtual ~WaterTank() ;virtual void fill();virtual void startDraining();virtual void stopDraining();void startHeating();void stopHeating();Temperature currentTemperature() const;protected:…};class NutrientTank : public StorageTank (public:NutrientTank() ;virtual ~NutrientTank() ;virtual void startDrainingt();virtual void stopDraining();protected:…};Класс StorageTank — это базовый класс иерархии.
Он обеспечивает структуру и поведениеобщие для всех емкостей: возможность их наполнять или опустошать. Классы WaterTank (емкостьдля воды) и NutrientTank (для удобрений) наследуют свойства StorageTank, частичнопереопределяют их и добавляют кое-что свое: например, класс WaterTank вводит новое поведение,связанное с температурой.Предположим, что мы имеем следующие описания:StorageTank si, s2;WaterTank w;NutrientTank n;Заметьте, переменные такие как s1, s2, w или n — это не экземпляры соответствующихклассов. На самом деле, это просто имена, которыми мы обозначаем объекты соответствующихклассов: когда мы говорим «объект s1» мы на самом деле имеем ввиду экземпляр StorageTank,обозначаемый переменной s1. Мы вернемся к этому тонкому вопросу в следующей главе.При проверке типов у классов, C++ типизирован гораздо строже. Под этим понимается,что выражения, содержащие вызовы операций, проверяются на согласование типов во времякомпиляции.
Например, следующее правильно:Level 1 = s1.level ();w.startDrainingt();n.stopDraining();Действительно, такие селекторы есть в классах, к которым принадлежат соответствующиепеременные. Напротив, следующее неправильно и вызовет ошибку компиляции:sl.startHeating(); // Неправильноn.stopHeating(); // НеправильноТаких функций нет ни в самих классах, ни в их суперклассах. Но следующееn.fill();совершенно правильно: функции fill нет в определении NutrientTank, но она есть ввышестоящем классе.Итак, сильная типизация заставляет нас соблюдать правила использования абстракций,поэтому она тем полезнее, чем больше проект.
Однако у нее есть и теневая сторона. А именно,даже небольшие изменения в интерфейсе класса требуют перекомпиляции всех его подклассов.Кроме того, не имея параметризованных классов, о которых речь пойдет в главах 3 и 9, труднопредставить себе, как можно было бы создать собрание разнородных объектов. Предположим, чтомы хотим ввести абстракцию инвентарного списка, в котором собирается все имущество, связанное с теплицей. Обычная для С идиома применима и в C++: нужно использовать класс-контейнер,содержащий указатели на void, то есть на объекты произвольного типа.class Inventory {public:Inventory();~Inventory();void add(void*) ;void remove(void*);void* mostRecent() const;void apply(Boolean (*)(void*));private:…};Операция apply — это так называемый итератор, который позволяет применить какуюлибо операцию ко всем объектам в списке.
Подробнее об итераторах см. в следующей главе.Имея экземпляр класса Inventory, мы можем добавлять и уничтожать указатели на объектылюбых классов. Но эти действия не безопасны с точки зрения типов — в списке могут оказатьсякак осязаемые объекты (емкости), так и неосязаемые (температура или план выращивания), чтонарушает нашу абстракцию материального учета. Более того, мы могли бы внести в списокобъекты классов WaterTank и TemperatureSensor, и по неосторожности ожидая от функцииmostRecent объекта класса WaterTank получить StorageTank.Вообще говоря, у этой проблемы есть два общих решения.
Во-первых, можно сделатьконтейнерный класс, безопасный с точки зрения типов. Чтобы не манипулировать снетипизированными указателями void, мы могли бы определить инвентаризационный класс,который манипулирует только с объектами класса TangibleAsset (осязаемого имущества), а этоткласс будет подмешиваться ко всем классам, такое имущество представляющим, например, кWaterTank, но не к GrowingPlan. Тем самым можно отсечь проблему первого рода, когданеправомочно смешиваются объекты разных типов. Во-вторых, можно ввести проверку типов входе выполнения, для того, чтобы знать, с объектом какого типа мы имеем дело в данный момент.Например, в Smalltalk можно запрашивать у объектов их класс. В C++ такая возможность невходила в стандарт до недавнего времени, хотя на практике, конечно, можно ввести в базовыйкласс операцию, возвращающую код класса (строку или значение перечислимого типа).
Однакодля этого надо иметь очень серьезные причины, поскольку проверка типа в ходе выполненияослабляет инкапсуляцию. Как будет показано в следующем разделе, необходимость проверки типаможно смягчить, используя полиморфные операции.В языках с сильной типизацией гарантируется, что все выражения будут согласованы потипу. Что это значит, лучше пояснить на примере. Следующие присваивания допустимы:s1 = s2;s1 = w;Первое присваивание допустимо, поскольку переменные имеют один и тот же класс, авторое — поскольку присваивание идет снизу вверх по типам. Однако во втором случаепроисходит потеря информации (известная в C++ как «проблема срезки»), так как класспеременной w, WaterTank, семантически богаче, чем класс переменной s1, то есть StorageTank.Следующие присваивания неправильны:w = s1; // Неправильноw = n; // НеправильноВ первом случае неправильность в том, что присваивание идет сверху вниз по иерархии, аво втором классы даже не находятся в состоянии подчиненности.Иногда необходимо преобразовать типы.
Например, посмотрите на следующую функцию:void checkLevel(const StorageTank& s);Мы можем привести значение вышестоящего класса к подклассу в том и только в томслучае, если фактическим параметром при вызове оказался объект класса WaterTank. Или вот ещеслучай:if (((WaterTank&)s).currentTemperature() < 32.0) ...Это выражение согласовано по типам, но не безопасно. Если при выполнении программывдруг окажется, что переменная s обозначала объект класса NutrientTank, приведение типа дастнепредсказуемый результат во время исполнения. Вообще говоря, преобразований типа надоизбегать, поскольку они часто представляют собой нарушение принятой системы абстракций.Теслер отметил следующие важные преимущества строго типизированных языков:• «Отсутствие контроля типов может приводить к загадочным сбоям в программах вовремя их выполнения.• В большинстве систем процесс редактирование-компиляция-отладка утомителен, ираннее обнаружение ошибок просто незаменимо.• Объявление типов улучшает документирование программ.• Многие компиляторы генерируют более эффективный объектный код, если им явноизвестны типы» [72].Языки, в которых типизация отсутствует, обладают большей гибкостью, но даже в такихязыках, по мнению Борнинга и Ингалса: «Программисты обычно знают, какие объекты ожидаютсяв качестве аргументов и какие будут возвращаться» [73].
На практике, особенно припрограммировании «в большом», надежность языков со строгой типизацией с лихвойкомпенсирует некоторую потерю в гибкости по сравнению с нетипизированными языками.Примеры типизации: статическое и динамическое связывание. Сильная и статическаятипизация — разные вещи. Строгая типизация следит за соответствием типов, а статическаятипизация (иначе называемая статическим или ранним связыванием) определяет время, когдаимена связываются с типами. Статическая связь означает, что типы всех переменных и выраженийизвестны во время компиляции;динамическое связывание (называемое также поздним связыванием) означает, что типынеизвестны до момента выполнения программы.
Концепции типизации и связывания являютсянезависимыми, поэтому в языке программирования может быть: типизация — сильная,связывание — статическое (Ada), типизация — сильная, связывание — динамическое (C++, ObjectPascal), или и типов нет, и связывание динамическое (Smalltalk). Язык CLOS занимаетпромежуточное положение между C++ и Smalltalk: определения типов, сделанные программистом,могут быть либо приняты во внимание, либо не приняты.Прокомментируем это понятие снова примером на C++.