|
|
From: Andrew Plyako <Andrew.Plyako@p20.f922.n5030.z2.fidonet.org>
Date: Sun, 05 Mar 2000 05:13:06 +0300
Turbo/Borland Pascal --> Delphi FAQ
Этот список FAQ вопросов написан в помощь тем, кто недавно перешел
на Delphi с таких продуктов, как Borland и Turbo Pascal. Здесь
рассмотрены основные вопросы, возникающие у людей, которые по тем или
иным причинам не смогли (или не захотели) прочитать серьезную
литературу, посвященную работе в Delphi. Так же этот список FAQ
вопросов рекомендуется тем, кто использует среду визуального
программирования Delphi, руководствуясь интуитивным представлением о
компонентах и какой-нибудь мало вразумительной книжкой (к таковым автор
относит, в частности, большинство книг В.В.Фаронова).
Этот список нетрадиционен по своей нацеленности на аудиторию, и
как следствие нетрадиционен по своему составу. Здесь содержатся ответы
не на вопрос _Как_ поступать в той или иной ситуации, а _Почему_ в той
или иной ситуации следует поступать так-то и так-то. В результате,
автор надеется, этот документ будет интерпретироваться не просто, как
список решения проблем, но и как некоторый лекционный материал, который
полезно прочитать от и до, вне зависимости от того, знаете ли вы ответ
на поставленные вопросы, или нет.
Кроме того, данный FAQ список нетрадиционен в плане соотношения
вопросов и ответов. Как Вы увидите, здесь приводятся достаточно
развернутые ответы, каждый из которых охватывает сразу несколько
потенциальных FAQ вопросов.
Итак...
(1) //Динамическое создание, отображение и уничтожение объектов
Q: Что такое класс? (Новое название для объекта?).
Q: Почему не работает старая схема динамического создания объекта?
Q: Что такое _утилита класса_?
Q: Почему везде рекомендуют использовать не деструктор а процедуру
Free?
Q: Почему динамически созданные визуальные компоненты VCL не
появляются на форме?
Q: Какая разница между Owner и Parent?
A: Прежде всего разберемся с определениями. К сожалению до
определенного времени в Паскале существовала путаница в понимании слова
"объект". Итак, отныне и навсегда (по крайней мере, покуда речь идет об
Object Pascal): описание структуры объекта, его полей и методов, мы
будем называть _классом_. Конкретный экземпляр (конкретный
представитель) класса мы будем называть объектом. Таким образом, класс
-- описание, объект -- конкретные значения, память машины, отведенная
под хранение данных.
TMyObject = class(TObject)
MyValue:byte
end;
var MyObject :TMyObject;
В указанном выше примере, TMyObject является классом, порожденным
от TObject (любой класс является потомком этого базового класса), а
MyObject -- объект. Причем объект, который (как и любой другой объект)
будет создан динамически, то есть в процессе выполнения программы. Но
заметим, что объект-объектом, однако, MyObject всего лишь... ссылка.
Строка "var MyObject" лишь определяет переменную, которая в дальнейшем
будет интерпретироваться, как ссылка на структуру, в которой хранятся
все данные об объекте (в первую очередь значения его полей). При этом
место в памяти под эту структуру не выделяется,-- ее выделение и
заполнение произойдет, как уже говорилось, в процессе выполнения
программы, точнее, только после вызова конструктора нашего объекта.
А теперь самое главное, как мы можем вызвать конструктор объекта,
если самого этого объекта еще нет, а есть только ссылка, непонятно на
что указывающая? Ощутили проблему? Еще раз: объявив MyObject мы
выделили 4 (?) байта для хранения ссылки. Ничего более мы не сделали.
Если в старом Паскале динамическое создание объекта подразумевало
создание ссылки на описанную структуру (точнее, выделение памяти под
эту структур) при помощи оператора new, то в Object Pascal'е,
динамическое создание происходит путем вызова конструктора. Но как
вызвать конструктор, если объект еще не создан? Получаем замкнутый
круг. А потому, не удивительно, что при запуске нашей программы
команда:
MyObject.Create
приведет к появлению красивого окошка с сообщением, о том, что наша
программа попыталась обратится в недоступную ей область памяти. Такую
табличку принято называть AV (Access Violation). Так что же делать?
Решение этой проблемы тесно связано с понятием _утилиты класса_.
Быть может, листая встроенное в Delphi справочное руководство или
исходники VCL, Вы уже натыкались на "странное" использование слова
class при описании методов какого-нибудь класса?
class function ClassName: ShortString;
Вот это и есть пресловутая _утилита класса_. Это метод, который может
прекрасно работать при отсутствии самого объекта. Так, вышеописанная
функция выдает имя класса, то есть для нашего самого первого примера,
это будет строка 'TMyObject'. Согласитесь, что для работы этой функции
сам экземпляр класса, то есть сам объект (конкретные значения его
полей) совсем не нужен. Раз такие методы не нуждаются в объекте, то и
вызывать их разумно не для объекта, а для класса:
MyShortString := TMyObject.ClassName;
Утилиту класса иногда называют "статическим методом". Это не
совсем правильно, но зато точно отражает физическую сущность утилит
класса: так же, как и в случае статического метода, адрес утилиты
класса в явном виде подставляется внутрь машинного кода программы, в то
место, где производится вызов утилиты класса (это осуществляется
компоновщиком при создании *.exe файла).
Конструктор описывается _без_ зарезервированного слова "class":
constructor Create;
однако, Вас это не должно смущать. По своей сути, конструктор это
именно _утилита класса_. Более того, конструктор выдает результат своей
"деятельности", а именно, он выдает ссылку на ту структуру, которую он
создал в памяти. То есть, конструктор выделяет необходимую область
памяти, заносит в нее требуемую информацию, а ссылку на эту структуру
выдает, как результат своей работы.
Суммируя все вышесказанное, можно понять, что правильный вызов
конструктора для нашего объекта MyObject должен иметь вид:
MyObject := TMyObject.Create;
Теперь хочется отметить, что _после_ создания объекта вызов его
конструктора ("MyObject.Create") допустим. При этом конструктор будет
работать как обычный метод, то есть будут выполнены только те действия,
которые описаны в конструкторе, например, инициализация каких-либо
полей.
Научившись создавать объект, неплохо бы научится его уничтожать.
По аналогии, логично предположить, что вместо использования деструктора
в совокупности с оператором dispose (как было принято в Паскале),
достаточно будет вызвать просто деструктор: MyObject.Destroy. Более
того, попытка поступить именно так приносит ожидаемые плоды: объект
благополучно уничтожается. Однако файл помощи рекомендует вместо
привычного деструктора использовать для уничтожения всех объектов
процедуру Free.
Ответ прост: разработчики позаботились о вас, описав у TObject
процедуру, которая перед уничтожением объекта производит
предварительную проверку его "существования". Другими словами, написав
MyObject.Free; MyObject:=nil; вы с гарантией не получите AV при
повторном вызове MyObject.Free. А вот вызов двух деструкторов Destroy
подряд приведет к ошибке (к тому самому AV).
Напоследок, опишем разницу между свойством Parent наследников
TWinControl и уже упомянутом свойстве Owner наследников TComponent. К
сожалению, нередко можно встретить недопонимание, между различиями этих
двух свойств, хотя разница проста и легко определима (правда,
определение получается громоздким и, подчас, неудобно читаемым).
Свойство Owner указывает на компонент, который является
_владельцем_ данного компонента. Если компонент является владельцем
других компонентов, то при своем уничтожении он уничтожит все те
компоненты, владельцами которых он является. У каждого компонента есть
свойство Components -- массив, в котором хранятся ссылки на все
компоненты владельцем которого является данный компонент. С другой
стороны, компонент может быть уничтожен сам по себе. Тогда, было бы
неплохо, если бы компонент сообщил об этом своему владельцу. Вот для
установления этой, обратной, связи и заводится свойство Owner.
Свойство Parent указывает на контрол (наследник TWinControl --
оконный элемент управления; фактически, обыкновенное окно, в понимании
Windows, то есть нечто, имеющее дескриптор hWnd), который является
_родителем_ данного контрола. Это означает, что данный контрол будет
прорисовываться (изображаться) именно на своем родителе. У каждого
контрола есть свойство Controls -- массив, в котором хранятся ссылки на
все контролы, родителем которого является данный контрол. При
динамическом создании контрола непременно надо указывать его родителя,
путем присвоения его свойству Parent:
MyButton:=TButton.Create(Form1); // Form1 -- владелец новой кнопки
MyButton.Parent:=Form2; // однако, рисоваться она будет на Form2
Подобное присвоение автоматом "обновит" массив Controls у своего
родителя.
Отметим, что уничтожение родителя данного контрола (уничтожение
формы Form2) также приводит к уничтожению самого контрола (к
уничтожению MyButton).
Наконец, заметим, что свойство Owner доступно только для чтения,
поэтому в отличии от Parent не может быть изменено, а задается один раз
(при вызове конструктора Create).
Итак, если Вы хотите динамически создать какой-нибудь визуальный
компонент VCL, например, как показано выше, MyButton, то надо не только
создать его при помощи конструктора Create, но и указать его родителя,
на котором этот компонент будет отображаться.
-+---------------------------------------------------------------------
(2) //Свойства и события. Динамическое задание событий.
Q: Что такое Self?
Q: Что такое свойство (property)?
Q: Чем, на самом деле, является событие (event)?
Q: Почему компилятор ругается на попытку задать свои обработчики
событий для TApplication: Application.OnActivate := MyProcedure; ?
И опять-таки, начнем с определений. К сожалению, нередко среди
программистов можно встретить недопонимание того, что такое "метод".
Это понятие уже встречалось в ответе (1), и там оно использовалось в
общепринятом смысле: метод -- это функция или процедура, которую мы
описываем внутри класса. Все так, однако, при таком понимании вопроса
от нашего внимания ускользает важный аспект: чем отличается процедура,
описанная внутри класса (то есть, метод) и процедура описанная "просто
так", например, внутри секции Implementation?
Заметим, что машинный код любого метода размещается в памяти лишь
один раз, вне зависимости от того, сколько экземпляров данного класса
мы создали. Однако метод должен, как минимум, работать с конкретными
значениями полей, т.е. с конкретным объектом! А значит, метод должен
"уметь" как-то "узнавать" какой объект его вызвал. Раз так, то не
удивительно, что "внутрь" любого метода всегда, "неявно", передается
указатель на объект, который вызывает этот метод. Этот указатель носит
имя Self, который, быть может Вами уже когда-нибудь встречался.
Естественно, в обычную процедуру подобный указатель не передается! Вот
в этом и заключается важное отличие просто процедуры, и
процедуры-метода.
Это половина того, что нам необходимо знать для осознания того,
что такое Event. Вторая требуемая половина -- это понятие Property
(свойство). Как гласит старинный принцип ООП, все действия с полями
объекта должны осуществляться только через методы этого объекта.
Property -- практическое воплощение этого принципа. Стандартное
описание свойства (Property):
property MyValue:Integer read GetValue write PutValue;
определяет, что чтение целочисленной (Integer) величины MyValue будет
осуществляться через функцию
function GetValue:Integer;
а запись -- будет производится процедурой
procedure PutValue(Value:Integer);
Разумеется функция GetValue и процедура PutValue должны быть описаны
как методы того же класса. После подобного описания свойства MyValue,
команда MyObject.MyValue:=10; вызовет процедуру PutValue с параметром
10. Разумеется, переданное процедуре значение желательно сохранять,
поэтому стандартный вид процедуры PutValue:
procedure TMyObject.PutValue(Value:Integer);
begin FValue:=Value; {Любые дополнительные действия} end;
где FValue -- поле класса TMyObject, определенное, как правило в
private секции (чтобы быть недоступной "пользователю"). Аналогичные
действия происходят и при обращении ResultValue:=MyObject.MyValue.
Соответственно, функция GetValue описывается как
function TMyObject.GetValue:Integer;
begin Result:=FValue; {Любые дополнительные действия} end;
В итоге, мы добились упомянутого выше принципа: вся работа с полем
FValue происходит через методы нашего класса (более того,
"пользователь" даже не догадывается о существовании поля FValue, он
работает только со свойством Value). Таким образом, принцип-принципом,
но такой подход позволяет скрыть от других людей методы и поля,
представляющие собой внутреннее устройство класса. К тому же, это дает
и немалую практическую пользу, например, если значение свойства связано
с какими-то другими характеристиками класса (объекта), которые должны
меняться при изменении самого свойства (примером может служить свойства
left и top большинства (если не всех) контролов, изменение значений
которых автоматически влечет изменения положения контрола).
Если же чтение или запись должны осуществляться прямо из/в какого-
то поля без каких-либо "дополнительных действий", то допускается такое
определение свойства:
property MyValue:Integer read FValue write FValue;
Итак, что же такое event? На самом деле, не более, чем свойство
процедурного типа:
private
FOnChange: TNotifyEvent;
public
property OnChange:TNotifyEvent read FOnChange write FOnChange;
где TNotifyEvent это процедурный тип. А именно
TNotifyEvent = procedure(Sender: TObject) of object;
Вроде бы все должно быть ясно, за исключением одного обстоятельства:
что означает "приписка" of object? Для этого вспомним начало нашего
обсуждения: раз эта процедура будет работать с конкретным объектом, то
"внутрь" нее должен передаваться указатель на объект, тот самый Self.
Так вот, of object объявляет новый процедурный тип, как тип таких
процедур, в которых передается указатель на объект. Именно поэтому
недопустимо присваивание вроде Application.OnActivate:=MyProcedure,
если MyProcedure не описана как метод какого-нибудь класса (компилятор
будет выдавать ошибку из-за несоответствия типа присваиваемой
процедуры). Более того, на момент присваивания объект того класса, в
котором описана MyProcedure должен быть уже создан! Иначе попытка
присваивания приведет к AV.
Как правило, (если речь идет о переопределении событий у
Application) такую процедуру описывают внутри главной формы проекта, а
само присваивание производят внутри обработчика события OnCreate этой
формы. Хорошей альтернативой служит такой вариант задания собственного
обработчика события для Application, прямо внутри dpr-файла:
type TActivateEvent = class
public
procedure MyActivate(Sender:TObject);
end;
begin
with TActivateEvent.Create do
begin
Application.OnActivate:=MyActivate;
Application.Initialize;
//..... создание форм
Application.Run;
Free
end
end;
-+---------------------------------------------------------------------
Этот список FAQ вопросов доступен по адресу:
http://www.adwin.ru/dinfo/?pas2html
Все отзывы, касательно данного списка FAQ, а также Ваши вопросы, ответы
на которые Вы хотели бы увидеть в этом списке FAQ, следует посылать
либо по NetMail: 2:5030/922.20@FidoNet.org,
либо по E-Mail: Dmitry.Plyako@paloma.spbu.ru
Andrew
© faqs.org.ru