лекции (2004) (1160823), страница 17
Текст из файла (страница 17)
1. сложная семантика свойств не видна для пользователя
2. в случае интеграции компонент (сред программирования)
Свойства есть в языках Дельфи и Cи#.
-
Дельфи
В Дельфи обычный член выражается
имя: тип;
Свойство выражается:
property
имя: тип;
read считыватель; // может быть имя другого члена-данного, либо имя функции-члена
function rd:тип
write писатель; // может быть имя другого члена-данного, либо имя процедуры-члена
procedure wr(value:тип);
После этого всего может стоять слово default (о свойствах по умолчанию позже).
Функция считыватель - возвращает значение. Функция писатель - записывает. Зачем вводить дополнительные члены?
-
пусть у нас есть:
m_I:integer;
-
мы вводим свойство:
property i:integer;
read m_i;
write m_i;
Если какой-нибудь смысл в таком объявлении? В таком коде особого смысла нет. Но надо иметь ввиду, что либо писать, либо читатель может быть опущен. Тогда мы можем, например, m_I закрыть на запись и открыть на чтение, и такой подход (использование свойств) очень удобен.
Ситуация, когда переменная закрывается на запись, очень распространена. В Обероне (где практически нет никаких излишек) был введен специальный механизм экспорта для членов-данных:
RECORD
X*-:INTEGER; //Х – является вообще-то публичным членом записи, но доступен только на чтение, но не на запись
В Си++ механизма свойств нет, и никто не спешит его вводить, так как пара функций get() и set() вполне решает проблему. Но в других языках, например в Обероне, необходим специальный механизм позволяющий закрывать переменную на запись. Почему так происходит? Обычно, значение переменной используется часто, если мы используем функцию типа get() она соответственно будет вызываться очень часто. Эта функция в простейшем случае возвращает закрытую или приватную переменную. В архитектуре современных процессоров большую роль играет процесс конвейеризации, когда мы спараллеливаем выполнение многих операций. Отметим, что сейчас повышение быстродействия процессора происходит не за счет увеличения гигагерц, а в результате усовершенствования их внутренней структуры и распараллеливания операций. Любой вызов функции, как и любой нелокальный переход, полностью гробит весь конвейер. В языке Си++ это решается при помощи inline-функций. Так если функция описана внутри класса, то она по умолчанию считается inline. inline - это указание компилятору, что вызов функции можно заменить ее телом, и никаких накладных расходов не будет. В Обероне это понятие отсутствует, поэтому ввели специальное решение для частной проблемы.
Свойства по умолчанию:
Если после свойства стоит ключевое слово default, то это означает, что в случае, если мы не знаем какое свойство на нужно, то компилятор подставляет это свойство. Что означает – мы не знаем, какое свойство нам нужно? Рассмотрим ситуацию:
class X
property I:T,
read … //в данный момент нас конкретные считыватели и писатели не интересуют
write…//в данный момент нас конкретные считыватели и писатели не интересуют
default;
end;
пусть у нас
Х а;
Т j;
а := Х.create();
j := a; // у нас переменные разных типов данных, отметим что в таком контексте нам от Х требуются данные типа Т. Вообще-то в такой ситуации компилятор дожжен ругаться на несоответствие типов данных. Но данном случае, если Х является классом, и у класса Х есть свойства типа Т, и это свойство является свойством по умолчанию, все это приводит к тому, что компилятор вставляет следующий код:
j := a.i; // получается, что тип данных слева и справа
один и тот же – Т
Это похоже на то, что мы рассматривали ранее j := (T)a. Это очень похоже на неявные преобразования. Таким образом, создатели языка Дельфи предусмотрели возможность неявных преобразований. Из класса Х всегда можно преобразовать в другой класс У, если в соответствующем классе опишем свойство типа У по умолчанию. Чаще всего такой механизм неявных свойств используется, если, например, компонента содержит в себе какой-либо контейнер, то имеет смысл делать этот контейнер свойством по умолчанию. Так класс одновременно проявляет черты некоторого контейнера, но и еще какие-то дополнительные черты. Хотя еще раз отметим: мы стоим на позиции, что никаких неявных преобразований в Дельфи нет.
Вопрос о перекрытии стандартных операций. Во все языках (кроме Оберона) есть перекрытие имён (одному и тому же имени соответствует несколько объектов). Компилятор различает эти объекты из контекста.
5)Перекрытие операций(формально не является свойством класса)
Перекрытие имен – overload (не путать с заменой), это означает, что одному имени соответствует несколько объектов в одной и той же области видимости. Решение о связывании имени с конкретным объектом всегда принимает компилятор исходя из контекста, связывание всегда статическое. В рассматриваемых нами языках понятие перекрытия есть (кроме Оберона), оно относится только к именам подпрограмм. Если речь идет об имени типа, переменной, константы – то правило: в одной области видимости одному имени – одна сущность.
С функциями по другому, имя функции обычно обозначает некоторую обобщенную операцию и этой операции может соответствовать несколько конкретных реализаций. Во всех языках есть полиморфные (многоформные) операции. Например, это операция +. (встроенные операции являются полиморфными). В большинстве случаев встроенные операции являются полиморфными, почему бы и программистам не разрешить создавать свои полиморфные операции?
Многие языки допускают перекрытие на имена функций и процедур, при этом требование такое: из контекста компилятор должен догадываться, о какой подпрограмме идет речь. Что такое контекст?
-
Для языков, основанных на понятии выражения (Си++), это только сам вызов функции ничего более (имя и параметры).
Стандартные операции выглядят так: operator(arg1, arg2);
Для операции + : operator +(arg1, arg2); или operator +(arg1);
То же самое и для Дельфи, и для Си#, и для Java. Как следствие, перекрытие происходит по числу и типу параметров. В языке Дельфи при перекрытии операции после объявлении процедуры мы должны указывать ключевое слово overload.
-
Более интересная ситуация в языке Ада. Используется перекрытие к именам литералов перечисления:
type T1 is (a, b, c);
T2 is (b, c, d); // это допустимо
Можно считать, что в такой ситуации речь идет о функциях, поскольку любую константу можно считать нульместной функцией, т.е. функцией без аргументов, которая возвращает фиксированное значение. В отличии от Си++, Си#, Java, где вызов функции сопровождается круглыми скобками, в Аде функцию или процедуру без параметров можно вызывать просто указывая ее имя.
В Аде понятие контекста вызова подпрограммы глубже. Контекстом вызова процедуры является сам оператор вызова процедуры, а контекстом вызова функции – выражение, внутри которого она вызывается.
-
z:= T1!b;// трактуй b как Т1
2)z:= c;// контекстом является вся конструкция, а именно оператор присваивания
так как c принадлежит и к Т1, и к Т2, то его тип в данном присваивании зависит от типа z
3)if f() then - здесь требуется функция, возвращающая логическое значение
В общем случае в Аде функции могут перекрываться не только по профилю своих параметров, но и по возвращаемому значению (если в Си++ функция без параметров, то перекрыть такую функцию мы не сможем).
А нельзя ли сделать пользовательскую семантику для стандартных операций? Здесь существуют две идеологии:
1) Да, это можно - идеология Ада, Cи#, Си++.
2) Нет, это нельзя - идеология Java, Дельфи
Все языки, которые мы рассматриваем, соблюдают ограничение на количество аргументов – сколько аргументов есть у стандартной операции, столько же должно быть у перекрытой. Это сделано для того, чтобы не менялся алгоритм синтаксического анализа (есть языки, которые как правило не компилируют, не поддерживающие это правило - PROLOG ).
Рассмотрим синтаксис:
Ада
function "+"
(X.T) return T;
(X1, X2:T) return T;
Заметим, что ни одном языке, который мы рассматриваем, нет ограничения на типы аргументов и на тип возвращаемого значения. Это может быть нехорошо, если в Си++ мы перекроем операцию присваивания как void- функцию. И тогда a=b=c; // ERROR
Можно еще сделать так: У& operator = (X&); когда надо сделать бы: X& operator = (X&). Кроме того плохим тоном считается придание перекрытиям специфического значения, т.е. операция сложения должна складывать, а не вычислять квадратный корень. Аналогично при перекрытии оператор "->" (ссылка на объект), он не должен, например, складывать свои аргументы.
Глава 5: Инкапсуляция и абстрактные типы данных
Обратите внимание:
Любой класс является типом данных (но не наоборот - для Си++, а для Си# - для любого тд есть класс из стандартной библиотеки). Есть понятие абстрактный класс и есть абстрактный тип данных. Но абстрактный класс - совсем не то, что абстрактный типа данных! Абстрактный класс (вот, например, для Си++ абстрактный класс – такой, у которого есть хотя бы одна виртуальная функция) имеет отношение к наследованию и к механизму динамического связывания. А абстрактный тип данных имеет отношение только к механизму инкапсуляции.
Сейчас мы поговорим об инкапсуляции. Инкапсуляция - упрятывание. Что можно упрятать и зачем это делать? Вспомним, что тип данных = множество значений + множество операций. Можно упрятывать структуры данных (целиком или по частям), плюс, как следствие, операции, которые есть процедуры или функции, привязанные к определенному тд. Пользователь не должен знать множество значений и то, как реализованы операции (тела соответствующих процедур и функций). У нас есть специальные конструкции – модули или классы, которые связывают воедино структуру данных и соответствующее множество операций. До этого не осуждалось, ограничен или нет доступ к членам модуля или класса. Об инкапсуляции речь зашла, когда разбирались модульные языки – разделение на модуль определений и модуль реализации для Модула 2 (либо на интерфейсную и реализационную части юнита в Дельфи, либо в спецификация и тело пакета в Аде). Вспомним, что все имена описанные в реализационной части упрятаны, так как они доступны только внутри модуля реализации.
Есть еще дополнительные возможности упрятывания. Абстрактным типом данных называется тип данных, множество значений которого упрятано от программиста. Работа с ним возможна только в терминах явно определенного множества операций (add() и т.п.). Можно сказать, что АТД – это множество операций, определенное явно.
Рассмотрим, как реализована инкапсуляция и АТД в разных ЯП, увидим, что наиболее полно все это реализовано в Аде. Кроме того, понятие инкапсуляции надо рассматривать отдельно применительно к модулям, и отдельно применительно к классам.
-
Единица защиты
Единицей защиты может быть тип целиком, либо член типа (часть типа). Модульные языки (Ада, Модула-2) ориентированы на то, что единица защиты - тип целиком (тип либо полностью открыт, либо полностью закрыт).
Если мы программируем без goto (т.е. терминах исключительно структур – данных и операторов) мы себя сознательно ограничиваем свои возможности, но при этом увеличивается понятность программ, скорость их написания, длина может уменьшаться. Аналогично и с упрятыванием, разграничение на множество значений и множество операций увеличивает качество кода и труда программиста. Не случайно, что первым из основных свойств объектно-ориентированных языков является понятие объектов и инкапсуляции. И только потом идут наследование и динамический полиморфизм.
Итак, ограничение на весь тип, программирование в терминах АТД – это особенности языка Ада (и модульных яп). В современных яп допускается оператор goto, и наличие классов определяет несколько иную тенденцию: единицей защиты является член типа, а именно член-данное. Если мы вспомним внеклассовый язык Оберон – там ситуация такая же, запись может быть целиком закрытая, частично открытая. Подразумевается, что лучше по возможности goto не использовать и программировать в терминах АТД.
-
Атом защиты
Атом защиты - весь тип и отдельные экземпляры. Атомом защиты всегда является весь тип. Нельзя сказать, что для данного экземпляра это поле является открытым, а для остальных закрытым, т.е. для всех экземпляров типа единообразны правила видимости или доступа.
-
Видимость или доступ
В чем тут различия. В языках, где ограничения на правила доступа, там видимыми являются все члены, но не ко всем членам разрешен доступ. Инкапсуляция выглядит так: видимыми с точки зрения компилятора и программиста являются все имена описанные, например, в классе. Все языки, в которых есть понятие класса, ориентированы на доступ. А модульные языки ориентированы на видимость. Разница между видимость и доступом проявляется именно при наследовании, а если наследования нет, то видимость и доступ эквивалентны.
Каким образом реализуется упрятывание в модульных ЯП: вся реализация – упрятана, всё, что в интерфейсе - всё открыто (Дельфи, Модула-2).
Небольшие отличия в языке Ада:
package P is
type T1 is array(1..N) of Real; // это целиком открытый тд