Denali. Предстоящей версии RAD Studio посвящается
Сиэтл, Берлин, Токио, Рио… Это не маршрут очередного прощальный тура Scorpions по городам и селам нашей планеты, а кодовые названия релизов последних версий Embarcadero RAD
Сиэтл, Берлин, Токио, Рио… Это не маршрут очередного прощальный тура Scorpions по городам и селам нашей планеты, а кодовые названия релизов последних версий Embarcadero RAD
Это перевод The sad but short story of the SM_AccessoriesName registry value. Автор: Реймонд Чен.
SM_AccessoriesName — ещё одна жертва программ, влезающих в недокументированные разделы реестра.
Это
Это перевод The sad but short story of the SM_AccessoriesName registry value. Автор: Реймонд Чен.
SM_AccessoriesName — ещё одна жертва программ, влезающих в недокументированные разделы реестра.
Это
Это перевод That time the CEO of a company complained to Congress about Windows file extensions. Автор: Реймонд Чен.
В те времена, когда ненавидеть Microsoft было
Это перевод That time the CEO of a company complained to Congress about Windows file extensions. Автор: Реймонд Чен.
В те времена, когда ненавидеть Microsoft было
«Цифры в Tiobe немного похожи на отображение мировой популяции диких кабанов, основываясь исключительно на том, сколько мяса кабана продается в супермаркетах» © Jon L. Aasenden
Меня всегда
Иногда бывают ситуации, когда ваше приложение просто молча закрывается, и вы понятия не имеете почему. Как можно диагностировать подобные ситуации?
Приложение может завершаться:
В не столь отдалённой статье мы кратко обрисовали возможные случаи:
TerminateProcess
или ExitProcess
(прямо или опосредованно — например, через Halt
).TerminateThread
или ExitThread
). Это не бывает в Delphi, поскольку компилятор Delphi вставляет неявный вызов Halt
в конец главного потока (т.е. всегда вызывает ExitProcess
в конце работы главного потока), но это может случиться, если внешний процесс уничтожит главный поток в вашей программе.Диагностику можно проводить внутри процесса, установив хуки на ключевые функции:
Диагностику можно проводить из внешнего процесса:
Вы можете использовать EurekaLog для диагностики как из, так и извне процесса. Сделать вы это можете даже не покупая лицензию. Для диагностики изнутри процесса вы можете использовать редакцию Trial, а для диагностики извне процесса — бесплатный EurekaLog Tools Pack.
%APPDATA%Neos Eureka S.r.lEurekaLogBug Reports
). Если вы меняли путь для папки с отчётами — открывайте его;Bug ReportsProject1.exeProject1_ExitLog.el
.Несколько замечаний:
Halt
(штатный выход);TerminateProcess
, но логически это — штатно. Например, при перезапуске по исключению. Поэтому будьте осторожны, если вы хотите показывать какой-то диалог при старте приложения при обнаружении отчёта о выходе;RtlReportSilentProcessExit
(только Vista+), либо (Windows XP и ранее) хуками на TerminateProcess
и TerminateThread
;Когда вы получили отчёт — открываете его как обычно, в EurekaLog Viewer. В отчёте будет стек вызова в момент выхода из приложения, например:
ExceptionLog7.ProcessExitHandler
2244[17]EInject.RtlReportSilentProcessExitHook
1166[12]kernel32.TerminateProcess
Unit175.TForm1.Button1Click
43[1]Controls.TControl.Click
Вместе с EurekaLog, а также с EurekaLog Tools Pack устанавливается утилита Threads Snapshot, которая предназначена для захвата стеков всех потоков приложения в определённый момент времени.
Вы можете зарегистрировать утилиту Threads Snapshot в качестве внешнего отладчика для мониторинга выхода из процесса:
C:Program Files (x86)Neos Eureka S.r.lEurekaLog 7Bin
(или Bin64, если у вас — 64-разрядное приложение) под учётной записью администратора;threadssnapshot.exe "/watch=Project1.exe"
Где Project1.exe
— имя вашей программы. Это может быть просто имя файла или полный путь к файлу. Эта команда зарегистрирует утилиту Threads Snapshot для мониторинга выхода из указанного процесса. Не закрывайте консоль, она ещё пригодится;
threadssnapshot.exe "/unwatch=Project1.exe"
Где Project1.exe
— в точности тот же параметр, который вы указывали в п2. Эта команда отменит регистрацию мониторинга.
В результате будет создан обычный EurekaLog отчёт, в котором стек может выглядеть как-то так:
ntdll.NtWaitForSingleObject
kernel32.TerminateProcess
Unit175.TForm1.Button1Click
43[1]Controls.TControl.Click
Технически, эта функциональность реализована через Global Flags.
Вот список того, что вы можете попробовать сделать для дополнительной диагностики.
Примечание: в списке ниже ключ реестра Windows Error Reportingчто-то
обозначает ключ HKCUSoftwareMicrosoftWindowsWindows Error Reportingчто-то
, а при его отсутствии — HKLMSoftwareMicrosoftWindowsWindows Error Reportingчто-то
, либо HKLMSoftwareWow6432NodeMicrosoftWindowsWindows Error Reportingчто-то
(для 32-битных приложений на 64-битной машине).
AeDebug
, либо хотя бы сбросьте параметр Auto
в 0.WerSvc
) не отключена (не находится в состоянии Disabled; по-умолчанию у неё тип запуска — Manual, но для надёжности вы можете её запустить вручную).DefaultConcent
= 1). Не забудьте проверить как политики машины, так и пользователя.Windows Error ReportingDebugApplications*
нет или он установлен в 1.Windows Error ReportingDontShowUI
нет или он установлен в 0.Windows Error ReportingLoggingDisabled
нет или он установлен в 0.SetErrorMode
с одним из следующих флагов: SEM_FAILCRITICALERRORS
, SEM_NOGPFAULTERRORBOX
, SEM_NOOPENFILEERRORBOX
. Для надёжности сделайте вызов SetErrorMode(0);
первым действием при запуске своего приложения.SetThreadErrorMode
с одним из следующих флагов: SEM_FAILCRITICALERRORS
, SEM_NOGPFAULTERRORBOX
, SEM_NOOPENFILEERRORBOX
для ваших потоков. Для надёжности сделайте вызов SetThreadErrorMode(0);
первым действием ваших потоков.WerSetFlags(WER_FAULT_REPORTING_NO_UI);
.WerSetFlags(WER_FAULT_REPORTING_ALWAYS_SHOW_UI);
первым действием при запуске своего приложения.System.JITEnable
равна 0. Для надёжности присвойте её 0 первым действием при старте приложения.%APPDATA%MicrosoftWindowsWERReportArchive
/ %APPDATA%CrashDumps
.SetUnhandledExceptionFilter
.TerminateProcess
, ExitProcess
, а если это не помогло — то и на TerminateThread
и ExitThread
.kernel32.KiUserExceptionDispatcher
— если эта функция будет вызвана непосредственно перед вылетом, то 99% за то, что у вас произошло крайне серьёзное необработанное исключение, при котором система даже не смогла показать сообщение.TerminateProcess
, TerminateThread
, чтобы узнать, не завершает ли ваш процесс кто-то ещё.Товарищи, я тут нашёл в черновиках статью аж от 2013 года. Публикую.
Статья написана сразу после «Дружественность» в Delphi. Статья является логическим продолжением серии переводов Полиморфизм ad nauseum и последующего обсуждения в Delphi-блогах.
За давностью лет я уж и забыл, почему она в черновиках. Возможно, не всё сказал, что хотел. Может, творческий запал оборвался. А может, местами коряво получилось, не вычитал. Там в конце было что-то про журнал — возможно, я планировал опубликовать это в журнале. Также, материал про соединение потоков и файлов планировался в серию про сериализацию. Сырцы к статье чудом нашёл в бэкапе проектов. Короче, сделайте скидку.
Полиморфизм является одним из ключевых понятий (наравне с инкапсуляцией, абстракцией и наследованием) для объектно-ориентированного программирования (ООП). Хотя, конечно, полиморфизм не является эксклюзивным свойством именно ООП. Тем не менее, в этой статье мы будем говорить практически только про ООП.
ООП зародилось давно — в конце 50-х/начале 60-х годах прошлого века. Сначала концепция объектов выражалась доступными средствами языков программирования, а потом она была закреплена и в синтаксисе языков. Первым таким языком стала Симула (середина 60-х). В ней были многие современные возможности: класс, объекты, виртуальные методы и т.д. Тем не менее, более 30 лет парадигма ООП оставалась в тени, не признанная сообществом программистом. Действительно, если вашей программе нужно сделать выборку из базы данных, затем что-то посчитать и составить отчёт, то тут не так уж много возможностей для ООП. Ситуация кардинально поменялась в начале-середине 90-х годов прошлого века — в связи с развитием графических интерфейсов. Графический интерфейс — штука достаточно сложная, здесь требуется манипулировать большим количеством разнообразных элементов. А ООП позволяет это здорово упростить. Возможности ООП привлекли внимание разработчиков, и с тех пор ООП является доминирующей концепцией (иными словами, количество языков программирования, реализующих объектно-ориентированную парадигму, является наибольшим по отношению к другим парадигмам).
Итак, раз уж исторически ООП популяризировалось именно за счёт графического интерфейса, то почему бы нам не начать с ООП и примера на графический интерфейс?
ООП строится на понятии классов и объектов. Класс — это своего рода шаблон, «проект дома на бумаге». Он определяет методы, свойства и события. По этим шаблонам создаются объекты. Объект — это экземпляр класса, «конкретный дом». Это некая цельная сущность, соединяющая воедино данные и методы по управлению ими. У одного класса может быть много объектов, но каждый объект принадлежит лишь одному классу. Все объекты одного класса будут иметь одинаковый набор свойств, методов и событий, но значения свойств и назначенные обработчики могут отличаться:
// Класс ("проект дома", тип данных):
type
TMyButton = class
procedure Click;
end;
// Объекты ("дома, построенные по проекту", переменные типа данных):
var
OKButton: TMyButton;
CancelButton: TMyButton;
HelpButton: TMyButton;
begin
// "Проект" говорит, что можно щёлкать:
OKButton.Click;
end;
// Выполняется для OK, Cancel и Help
procedure TMyButton.Click;
begin
// ...
end;
Одна из самых больших проблем с ООП — научиться думать в терминах объектов. Как правило, человек, не знакомый с ООП, видит перед собой «просто полотно кода». Сообразить, что этот код ассоциируется с каким-то объектом, имеет структуру — именно это сложно. Иными словами, для него нет разницы между Click
и TMyButton.Click
.
Чтобы научиться мыслить в терминах объектов, нужно думать абстрактно, а не конкретно. Если вы удачно выберете абстракцию, то система будет представлена чёткой картиной, в которой будет легко разобраться. Уменьшение сложности понимания кода достигается сокрытием реализации.
К примеру, в примере выше у нас есть абстракция — «кнопка». Вы можете её создать, вы можете на ней «щёлкнуть» (Click). При этом вам не нужно думать: «а как же это работает? Что нужно сделать в коде, чтобы создать кнопку? А щелчок — это что же: мышью на неё навести и нажать кнопку?».
Мы можем «бросать» на форму разные визуальные элементы управления: кнопки, списки, поля ввода и так далее. Каждый из них уникален, он выглядит и ведёт себя индивидуально, по-разному. Тем не менее, все они должны уметь позиционировать себя на форме, все они должны уметь себя рисовать, и, как правило, все они поддерживают отображение/ввод заголовка (Caption/Text).
Сказанное означает, что концептуально у нас есть общая сущность — «элемент управления», которая умеет себя рисовать, задавать своё положение и указывать заголовок (текст), но каждый конкретный элемент управления будет реализовывать эти общие свойства по своему. Итого, в терминах ООП у нас получаются такие структуры данных:
type
// Общий класс
TMyControl = class
strict private
function GetBounds: TRect;
procedure SetBounds(const AValue: TRect);
function GetText: String;
procedure SetText(const AValue: String);
public
// Публичный интерфейс, контракт - то, что должен уметь делать объект этого класса:
property Bounds: TRect read GetBounds write SetBounds;
property Text: String read GetText write SetText;
procedure Draw;
end;
// Несколько примеров конкретных классов:
TMyButton = class(TMyControl)
end;
TMyEdit = class(TMyControl)
end;
TMyLabel = class(TMyControl)
end;
Конечно же, каждый конкретный класс должен указывать, как он будет располагаться, как он будет рисоваться, как он будет использовать свой текст. Вот здесь на сцену и выходит полиморфизм.
В Delphi есть много технических способов обеспечить полиморфизм — полный список был приведён в предыдущей статье. Если мы говорим про ООП, то основными способами будут:
Вот давайте на них и посмотрим.
Чтобы использовать полиморфизм, вам нужно иметь класс с методом. Этот метод вы должны объявить виртуальным, указав ключевое слово virtual
:
type
TMyControl = class
// Объявление виртуального метода
procedure Draw; virtual;
end;
После этого вы можете создать наследник класса, в котором вы можете заместить реализацию метода на свою собственную, отличную от унаследованной. Чтобы указать на замещение реализации, вам нужно использовать ключевое слово override
:
type
TMyButton = class(TMyControl)
// Замещение виртуального метода в классе-наследнике
procedure Draw; override;
end;
Разумеется, виртуальный метод для замещения должен быть доступен классу-наследнику — т.е. он должен находится в любой секции, кроме private
и strict private
.
Тогда при вызове метода у базового класса будет вызываться не его реализация, а замещённая реализация в наследнике. Например:
type
TMyControl = class
procedure Draw; virtual;
end;
TMyButton = class(TMyControl)
procedure Draw; override;
end;
TMyEdit = class(TMyControl)
procedure Draw; override;
end;
var
Controls: array of TMyControl;
begin
SetLength(Controls, 2);
Controls[0] := TMyButton.Create;
Controls[1] := TMyEdit.Create;
for X := 0 to High(Controls) do
// Вызовет сначала TMyButton.Draw, а затем TMyEdit.Draw
Controls[X].Draw;
end;
В чём же здесь разница между виртуальными (полиморфными) методами и обычными статическими (не полиморфными) методами?
var
Control1: TMyButton;
Control2: TMyEdit;
begin
// Если метод Draw - статический:
Control1 := TMyButton.Create;
Control1.Draw; // вызывает TMyButton.Draw
FreeAndNil(Control1);
Control2 := TMyEdit.Create;
Control2.Draw; // вызывает TMyEdit.Draw
FreeAndNil(Control2);
// Если метод Draw - виртуальный:
Control1 := TMyButton.Create;
Control1.Draw; // вызывает TMyButton.Draw
FreeAndNil(Control1);
Control2 := TMyEdit.Create;
Control2.Draw; // вызывает TMyEdit.Draw
FreeAndNil(Control2);
end;
Здесь кажется, что разницы нет. Дело в том, что разница видна именно при использовании базового класса для ссылки на конкретный класс:
var
Control: TControl;
begin
// Если метод Draw - статический:
Control := TMyButton.Create;
Control.Draw; // вызывает TMyControl.Draw
FreeAndNil(Control);
Control := TMyEdit.Create;
Control.Draw; // вызывает TMyControl.Draw
FreeAndNil(Control);
// Если метод Draw - виртуальный:
Control := TMyButton.Create;
Control.Draw; // вызывает TMyButton.Draw
FreeAndNil(Control);
Control := TMyEdit.Create;
Control.Draw; // вызывает TMyEdit.Draw
FreeAndNil(Control);
end;
Иными словами, вам не нужен полиморфизм, когда вы хотите работать с одним конкретным объектом. Но как только у вас на сцене появляется несколько разных объектов с общими свойствами или поведением, и вам нужно сделать одно действие для всех объектов (или хранить их в общем списке или ещё что-то общее) — вот именно тут проявляется полиморфизм.
Примечание: наряду с виртуальными методами в Delphi есть динамические методы. С точки зрения поведения они ничем не отличаются от виртуальных. Разница между ними в том, что виртуальные вызовы оптимизированы на скорость работы, а динамические методы оптимизированы на минимальные размер занимаемой памяти. В современных условиях вам следует всегда использовать виртуальные методы, т.к. сегодня оптимизация по скорости представляется более ценной, чем оптимизация по размеру.
Итак, с этими знаниями теперь мы можем обновить наш исходный пример например так:
type
// Общий класс
TMyControl = class
strict private
FText: String;
FBounds: TRect;
strict protected
function GetBounds: TRect; virtual;
procedure SetBounds(const AValue: TRect); virtual;
function GetText: String; virtual;
procedure SetText(const AValue: String); virtual;
public
// Публичный интерфейс, контракт - то, что должен уметь делать объект этого класса:
property Bounds: TRect read GetBounds write SetBounds;
property Text: String read GetText write SetText;
procedure Draw; virtual; abstract;
end;
// Несколько примеров конкретных классов:
TMyButton = class(TMyControl)
public
procedure Draw; override;
end;
TMyEdit = class(TMyControl)
public
procedure Draw; override;
end;
TMyLabel = class(TMyControl)
public
procedure Draw; override;
end;
{ TMyControl }
function TMyControl.GetBounds: TRect;
begin
Result := FBounds;
end;
procedure TMyControl.SetBounds(const AValue: TRect);
begin
FBounds := AValue;
Draw;
end;
function TMyControl.GetText: String;
begin
Result := FText;
end;
procedure TMyControl.SetText(const AValue: String);
begin
FText := AValue;
Draw;
end;
// Простейшие рисунки для элементов управления:
{ TMyButton }
procedure TMyButton.Draw;
begin
Brush.Color := clBtnFace;
Canvas.FillRect(Bounds);
Canvas.TextOutXY(Bounds.Left, Bounds.Top, Text);
end;
{ TMyEdit }
procedure TMyEdit.Draw;
begin
Brush.Color := clWhite;
Canvas.FillRect(Bounds);
Canvas.TextOutXY(Bounds.Left, Bounds.Top, Text);
end;
{ TMyLabel }
procedure TMyLabel.Draw;
begin
Canvas.TextOutXY(Bounds.Left, Bounds.Top, Text);
end;
Вы можете использовать ключевое слово abstract
, написав его после virtual
, чтобы указать на то, что у виртуального метода нет реализации. Действительно TMyControl
не умеет себя рисовать — он не является настоящим элементом управления, ему просто нечего рисовать. Вот почему мы объявили виртуальный метод абстрактным. Конкретные наследники TMyControl
должны обязательно заместить абстрактный метод, указав свою конкретную реализацию. Это не строго необходимо для просто виртуальных методов — которые можно замещать при необходимости, но можно и не замещать — если вас устраивает реализация по умолчанию в базовом классе. В этом примере нас устраивает реализация для свойств, поэтому мы замещаем только метод рисования.
Следует также упомянуть, что при замещении метода в классе вы имеете возможность вызвать предыдущую реализацию. Это удобно, если вы не хотите переписывать метод с нуля, а хотите лишь слегка модифицировать его. Это делается с использованием ключевого слова inherited
. Этот механизм не специфичен именно для виртуальных методов и может использоваться с любыми методами. В любом случае, в зависимости от реализации базового класса, класс-наследник может решить вызывать унаследованный метод в самом начале, перед выполнением своих действий, либо в середине (довольно редко), либо после своих действий, в конце, либо же не вызывать вовсе.
Существует два способа вызова унаследованного варианта метода, с тонкими отличиями:
procedure TRectangle.Draw(Canvas: TCanvas);
begin
inherited Draw(Canvas);
Canvas.Rectangle(FRect);
end;
Этот код безусловно вызовет унаследованный метод Draw
базового класса. Если метод в базовом классе — абстрактный, то этот вызов завершиться неудачей, возбуждая исключение EAbstractError
во время выполнения.
Альтернативный синтаксис вызова — просто написать inherited;
, например:
procedure TRectangle.Draw(Canvas: TCanvas);
begin
inherited;
Canvas.Rectangle(FRect);
end;
Этот код будет работать идентично предыдущему для случаев, когда базовый класс содержит не абстрактный метод. Если же метод базового класса является абстрактным, либо же базовый класс вообще не содержит метода (для не виртуальных методов), то вызов inherited
становится noop (No-Operation — пустым оператором). Компилятор не генерирует для него кода (и поэтому вы не можете поставить на него точку останова). Этот механизм является частью отличной версионной устойчивости языка Delphi. Достоинством же первого способа является возможность изменить аргументы к унаследованному вызову.
Message-методы являются разновидностью динамических методов. В основном они используются для диспетчеризации оконных сообщений, но в целом могут использоваться и более широко. Мы не будем рассматривать их в этой статье.
Помимо положения, текста и умения отрисовываться некоторые элементы управления должны реагировать на ввод пользователя. К примеру, кнопка должна уметь воспринимать щелчок пользователя по ней. Мы можем попытаться применить предыдущий подход:
type
TMyButton = class(TMyControl)
public
procedure Draw; override;
procedure Click; virtual;
end;
TMyOKButton = class(TMyButton)
public
procedure Click; override;
end;
TMyCancelButton = class(TMyButton)
public
procedure Click; override;
end;
procedure TMyButton.Click;
begin
// ничего не делать - простая кнопка игнорирует щелчок
end;
procedure TMyOKButton.Click;
begin
ModalResult := mrOK;
CloseDialog;
end;
procedure TMyCancelButton.Click;
begin
ModalResult := mrCancel;
CloseDialog;
end;
Конечно, такой подход не является жутко удобным. Вам нужно порождать новые классы для минимальных изменений в их поведении. Фактически, у вас будет по одному объекту каждого класса, потому что классы становятся слишком узкоспециализированными.
Здесь на сцену выходят события. Событие — это обычный процедурный указатель. Т.е. это указатель на код. Если вы введёте в класс свойство типа событие, то это будет означать, что объекты этого класса смогут менять не только свои данные (текст, положение и т.п.), но и поведение.
type
TClickEvent = procedure of object;
// или:
TClickEvent = procedure;
TMyButton = class(TMyControl)
strict private
FClickEvent: TClickEvent;
protected
procedure DoClick;
public
procedure Draw; override;
property OnClick: TClickEvent read FClickEvent write FClickEvent;
end;
procedure TMyButton.DoClick;
begin
if Assigned(FClickEvent) then
FClickEvent;
end;
// ...
procedure TMyDialog.OKClick;
// или:
procedure OKClick;
begin
ModalResult := mrOk;
CloseDialog;
end;
procedure TMyDialog.CancelClick;
// или:
procedure CancelClick;
begin
ModalResult := mrCancel;
CloseDialog;
end;
var
Dialog: TMyDialog;
OKButton: TMyButton;
CancelButton: TMyButton;
begin
OKButton := TMyButton.Create;
CancelButton := TMyButton.Create;
OKButton.OnClick := Dialog.OKClick;
// или:
OKButton.OnClick := OKClick;
CancelButton.OnClick := Dialog.CancelClick;
// или:
CancelButton.OnClick := CancelClick;
// ...
end;
В этом примере показаны события как в виде чистого процедурного указателя (procedure), так и в виде указателя на метод (procedure of object). Разница между ними состоит лишь в том, что первый может указывать только на обычную функцию или процедуру, а второй должен указывать только на метод объекта. В остальном эти два понятия идентичны.
Как вы видите из кода выше, событие состоит из двух частей: OnClick
— свойства процедурного типа (приёмник) и DoClick
— вызывающего метода (отправитель). В такой реализации (невиртуальный) метод DoClick
эквивалентен виртуальному методу Click
из предыдущего примера. Вы вызываете этот метод, когда вам нужно щёлкнуть по кнопке. Виртуальный Click
реализовывал полиморфизм замещением метода разными реализациями в наследниках класса. Событие же реализует полиморфизм путём назначения различных реализаций процедурному указателю.
Заметьте, что события в виде чисто процедурных указателей позволяют реализовывать полиморфное поведение не-ООП коду (например — процедурному).
Если вы вернётесь немного назад и посмотрите на пример с виртуальными методами, то заметите, что у нас там есть два класса: основной базовый и наследник (или несколько наследников). По сути задача базового класса в этом случае — сформировать контракт по взаимодействию с объектами этого класса. Сам по себе этот класс не содержит никакой уникальной реализации. Это просто служебный код.
Основная проблема здесь в том, что почти всегда объектам нужно удовлетворять нескольким контрактам. К примеру, объект может быть «элементом управления» — поддерживать позиционирование и уметь рисовать себя, объект может быть «текстовым элементом» — уметь отображать и/или вводить текст, объект может быть «кликабельным» — уметь реагировать на щелчки и так далее. В рамках ООП и наследование это решается построением правильного дерева наследования. Вот пример настоящего дерева наследования из Delphi (фрагменты):
Не всегда это можно сделать удачно. Иногда бывают ситуации, когда класс с аналогичной функциональностью вынужден создавать свою собственную ветку — только из-за того, что какая-то деталь реализации отлична. Вот, к примеру, дерево для TSpeedButton
:
Обратите внимание, что несмотря на то, что это — кнопка (и, значит, по логике должна наследоваться от TCustomButton
или TButton
), TSpeedButton
наследуется от совершенно не связанной ветки дерева — и всё потому, что она является пользовательским, а не оконным контролом: к примеру, у неё нет оконного описателя (который вводится в TWinControl
— предка TButton
).
Ещё более ярко это проявляется в современных версиях Delphi — в них наравне с VCL появляется новая библиотека элементов управления: FireMonkey. FireMonkey вынуждена строить своё, полностью изолированное дерево наследования, во многом повторяющее дерево наследования VCL:
(Примечание: хотя на этом рисунке есть и TControl
и TButton
— как и на предыдущем, но надо понимать, что это — совершенно другие классы, которые не имеют ничего общего. Они просто имеют одинаковое имя.)
Подобные несуразности легко решаются интерфейсами. Интерфейс — это контракт в чистом виде, без реализации. Иными словами, главное отличие класса от интерфейса — в том, что класс состоит из интерфейса и реализации. Это означает, что к интерфейсу мы можем легко присоединить любую реализацию — а это и есть полиморфизм. Ключевой фактор здесь — единственная сущность (для ООП — объект) может реализовывать сколько угодно интерфейсов. Вот пример из иерархии Delphi:
Иными словами, если бы VCL и FireMonkey были бы написаны на интерфейсах, то вместо дерева наследования у нас был бы набор интерфейсов вроде:
type
IPositionableControl = interface
['{4E916E73-AC46-4634-BE93-BD95B5ACB083}']
function GetBounds: TRect;
procedure SetBounds(const AValue: TRect);
property Bounds: TRect read GetBounds write SetBounds;
end;
ICaptionableControl = interface
['{7254A2E7-15D2-4374-BB22-7EED602B687B}']
function GetText: String;
procedure SetText(const AValue: String);
property Text: String read GetText write SetText;
end;
IVisualControl = interface
['{EAC5B888-CA54-4342-BC6A-9D4404C0C0CE}']
procedure Draw;
end;
IClickableControl= interface
['{EAC5B888-CA54-4342-BC6A-9D4404C0C0CE}']
function GetClick: TNotifyEvent;
procedure SetClick(const AValue: TNotifyEvent);
procedure OnClick: TNotifyEvent read GetClick write SetClick;
end;
IWinControl = interface
['{9D1D9651-D473-4BDB-A77F-641D4399DF76}']
function GetBounds: HWND;
property Handle: HWND read GetHandle;
end;
ICrossPlatformControl = interface
['{9D1D9651-D473-4BDB-A77F-641D4399DF76}']
function GetBounds: Pointer;
property Handle: Pointer read GetHandle;
end;
ICustomControl = interface
['{2DF08C79-6DD1-4E90-810B-FD311C8BFA3F}']
function GetCanvas: TCanvas;
property Canvas: TCanvas read GetCanvas;
end;
Тогда TButton
от VCL реализовывал бы IPositionableControl
, ICaptionableControl
, IVisualControl
, IClickableControl
и IWinControl
, но не ICrossPlatformControl
и не ICustomControl
. TSpeedButton
из VCL реализовывал бы IPositionableControl
, ICaptionableControl
, IVisualControl
, IClickableControl
и ICustomControl
, но не IWinControl
и не ICrossPlatformControl
. А TButton
от FireMonkey — IPositionableControl
, ICaptionableControl
, IVisualControl
, IClickableControl
и ICrossPlatformControl
, но не IWinControl
и не ICustomControl
.
Это добавляет в код высокую степень полиморфизма, т.к. теперь все кнопки становятся кнопками — вне зависимости от того, из VCL они или из FireMonkey, оконные они или нет. Теперь можно писать код, который работает с кнопками вообще (например, щёлкает по ним). И он (код) будет одинаков для любых библиотек и любых реализаций.
К сожалению, изначально VCL была написана в те времена, когда интерфейсов в языке Delphi не существовало. Поэтому она и FireMonkey написаны на объектах (обе библиотеки разделяют некоторые общие части). Тем не менее, в своём коде от нас никто не требует использовать именно объекты, так что мы можем писать гибкий (полиморфный) код, используя интерфейсы.
Для начала вам нужно описать сам интерфейс. По аналогии с примером для виртуальных методов:
type
IMyControl = interface
['{FAFE2359-4D4D-42BB-89EC-2300E3E22FAC}']
function GetBounds: TRect;
procedure SetBounds(const AValue: TRect);
function GetText: String;
procedure SetText(const AValue: String);
property Bounds: TRect read GetBounds write SetBounds;
property Text: String read GetText write SetText;
procedure Draw;
end;
У интерфейсов каждый метод всегда обязательно является виртуальным и абстрактным — поэтому нам не нужно использовать никаких дополнительных ключевых слов.
Далее необходимо интерфейс реализовать. Реализацию интерфейса в Delphi синтаксически удобно делать классом.
type
TMyButton = class(TObject, IMyControl)
public
// IInterface
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
// IMyControl
function GetBounds: TRect;
procedure SetBounds(const AValue: TRect);
function GetText: String;
procedure SetText(const AValue: String);
procedure Draw;
end;
Из этого кода можно отметить несколько моментов:
TObject
, так любой интерфейс автоматически наследуется от IInterface
(он же — IUnknown
).С учётом сказанного мы приходим к:
type
TMyControl = class(TInterfacedObject)
private
FBounds: TRect;
FText: String;
protected
function GetBounds: TRect;
procedure SetBounds(const AValue: TRect);
function GetText: String;
procedure SetText(const AValue: String);
procedure Draw; virtual; abstract;
end;
TMyButton = class(TMyControl, IMyControl)
protected
procedure Draw; override;
end;
TMyEdit = class(TMyControl, IMyControl)
public
procedure Draw; override;
end;
TMyLabel = class(TMyControl, IMyControl)
public
procedure Draw; override;
end;
Обратите внимание, что метод Draw
сделан абстрактным и виртуальным только по той причине, что его вызывают другие методы TMyControl
. Если же вам его вызывать не нужно, то из класса TMyControl
его можно убрать. Полиморфизм в данном случае заключается не в использовании слова virtual, а в проецировании реализаций на интерфейс. Возможно, что более наглядно это будет видно в таком примере:
type
TMyButton = class(TInterfacedObject, IMyControl)
function GetBounds: TRect;
procedure SetBounds(const AValue: TRect);
function GetText: String;
procedure SetText(const AValue: String);
procedure Draw;
end;
TMyButton2 = class(TMyButton, IMyControl)
procedure Draw;
end;
TMyButton3 = class(TMyButton, IMyControl)
procedure Draw;
end;
var
Control: IMyControl;
begin
Control := TMyButton.Create;
Control.Draw; // вызывает TMyButton.Draw
Control := TMyButton1.Create;
Control.Draw; // вызывает TMyButton1.Draw
Control := TMyButton2.Create;
Control.Draw; // вызывает TMyButton2.Draw
end;
Впрочем иногда бывает удобнее использовать и виртуальные методы:
type
TMyButton = class(TInterfacedObject, IMyControl)
function GetBounds: TRect; virtual;
procedure SetBounds(const AValue: TRect); virtual;
function GetText: String; virtual;
procedure SetText(const AValue: String); virtual;
procedure Draw; virtual;
end;
TMyButton2 = class(TMyButton)
procedure Draw; override;
end;
TMyButton3 = class(TMyButton)
procedure Draw; override;
end;
var
Control: IMyControl;
begin
Control := TMyButton.Create;
Control.Draw; // вызывает TMyButton.Draw
Control := TMyButton1.Create;
Control.Draw; // вызывает TMyButton1.Draw
Control := TMyButton2.Create;
Control.Draw; // вызывает TMyButton2.Draw
end;
Обратите внимание на отличия:
IMyControl
Помните наглядную иллюстрацию из предыдущей статьи, где входит начальник и отдаёт команду работать? Вот как это могло бы выглядеть на практике:
type
// Абстрактный работник/сотрудник
TWorker = class
public
procedure Work; virtual; abstract;
end;
// Ниже - четыре конкретных сотрудника
// Секретарь
TSecretary = class(TWorker)
public
procedure Work; override;
end;
// Менеджер
TSalesManager = class(TWorker)
public
procedure Work; override;
end;
// Юрист
TLawyer = class(TWorker)
public
procedure Work; override;
end;
// Программист
TDeveloper = class(TWorker)
public
procedure Work; override;
end;
// Начальник (не является работником)
TBoss = class
public
// крикнуть "Работать!"
procedure ShoutWork;
end;
procedure TSecretary.Work;
begin
// печатать на клавиатуре
end;
procedure TSalesManager.Work;
begin
// схватиться за телефон
end;
procedure TLawyer.Work;
begin
// уткнуться в документы
end;
procedure TDeveloper.Work;
begin
// тестировать код
end;
var
// Сотрудники в офисе:
OfficeWorkers: array of TWorker;
procedure TBoss.ShoutWork;
begin
for X := 0 to High(OfficeWorkers) do
OfficeWorkers[X].Work;
end;
Если бы у нас не было бы полиморфизма, то у вас был бы такой код:
type
// Секретарь
TSecretary = class
public
procedure StartTyping;
end;
// Менеджер
TSalesManager = class
public
procedure GetOnThePhone;
end;
// Юрист
TLawyer = class
public
procedure LookIntoDocuments;
end;
// Программист
TDeveloper = class
public
procedure CreateCode;
end;
// Начальник
TBoss = class
public
// крикнуть "Работать!"
procedure ShoutWork;
end;
procedure TSecretary.StartTyping;
begin
// печатать на клавиатуре
end;
procedure TSalesManager.GetOnThePhone;
begin
// схватиться за телефон
end;
procedure TLawyer.LookIntoDocuments;
begin
// уткнуться в документы
end;
procedure TDeveloper.CreateCode;
begin
// тестировать код
end;
var
Secretaries: array of TSecretary;
SalesManagers: array of TSalesManager;
Lawyers: array of TLawyer;
Developers: array of TDeveloper;
procedure TBoss.ShoutWork;
begin
for X := 0 to High(Secretaries) do
Secretaries[X].StartTyping;
for X := 0 to High(SalesManagers) do
SalesManagers[X].GetOnThePhone;
for X := 0 to High(Lawyers) do
Lawyers[X].LookIntoDocuments;
for X := 0 to High(Developers) do
Developers[X].CreateCode;
end;
Впрочем, последний блок кода можно переписать с общим списком так:
var
Workers: array of TObject;
procedure TBoss.ShoutWork;
begin
for X := 0 to High(Workers) do
if Workers[X] is TSecretary then
TSecretary(Workers[X]).StartTyping
else
if Workers[X] is TSalesManager then
TSalesManager(Workers[X]).GetOnThePhone
else
if Workers[X] is TLawyer then
TLawyer(Workers[X]).LookIntoDocuments
else
if Workers[X] is TDeveloper then
TDeveloper(Workers[X]).CreateCode
else
Assert(False);
end;
В любом случае код стал больше и запутанней. В нём стало тяжелее ориентироваться и уже не так ясно, что же происходит. Что ещё хуже: если компания нанимает уборщика, то вам придётся переписать весь код в программе, который работает со списками (или списком) работников, добавив в него код для нового типа сотрудника. В первом же варианте (с полиморфизмом), чтобы нанять в компанию уборщика — вам достаточно создать для него класс и добавить объекты этого класса в список сотрудников. Всё. Никакой код изменять не нужно. Благодаря полиморфизму весь уже написанный код будет уметь работать с уборщиками — просто потому, что он работает с абстрактным понятием: «сотрудник», а не с конкретными представителями.
Из этих примеров хорошо видно, что если:
if ... then ... else if ... then ... else ...
(или же case
)…то почти всегда это означает, что в вашем коде есть возможность применить полиморфизм, но вы ею не воспользовались.
Как уже было сказано — полиморфизм не является эксклюзивным свойством ООП. На самом нижнем уровне полиморфизм заключается в изменении адреса вызова в run-time. Поэтому, конечно же, существуют и способы реализовать полиморфное поведение, не используя ООП. К примеру, в процедурном подходе вы можете использовать указатели на код: процедурные типы.
type
TOperationProc = function(A, B: Integer): Integer;
var
Operation: TOperationProc;
...
C := Operation(A, B);
Здесь, в зависимости от того, что именно содержится в переменной Operation
, этот код может произвести сложение, вычитание, умножение или (целочисленное) деление.
К примеру, типичная операция: динамический импорт через GetProcAddress
уже иллюстрирует полиморфное поведение. В самом деле, вы импортируете функцию Windows API и она будет работать одинаково на любых системах. Внутренняя реализация может существенно изменится, но вы всегда будете получать желаемый результат.
На это можно посмотреть и с другой стороны. К примеру, программа с плагинами. Через GetProcAddress
вы получаете адрес функции плагина. Функция всегда одна, но её действие будет зависеть от плагина. Т.е. поведение функции меняется.
У меня даже есть практический пример для процедурного подхода.
В Delphi есть несколько способов работы с файлами: файлы Паскаля, потоки данных, объекты-оболочки (TStrings). Каждый способ имеет свои достоинства и недостатки. К примеру, достоинства файлов Паскаля:
Недостатки файлов Паскаля иллюстрируются достоинствами файловых потоков:
К счастью, начиная с Delphi XE2 в RTL появились средства, позволяющие полноценно соединить плюсы каждого из подходов. Что это за средства? В Delphi XE2 файлы Паскаля могут иметь ассоциированную с ними кодовую страницу, которая указывает, в какой кодировке нужно выводить текстовые данные. Среди прочих — поддерживается и UTF8, что позволяет нам полноценно реализовать поддержку Unicode. Теперь можно делать WriteLn('Русский текст')
— и это будет работать именно так, как ожидается.
Если вы посмотрите на плюсы и минусы каждого подхода, то увидите, что файлы Паскаля хорошо подходят для внешнего слоя (интерфейса): с ними удобно работать. А файловые потоки хорошо подходят для внутреннего слоя (реализации): они функциональны. Так как же нам соединить их?
Ответ можно найти в структуре (записи) TTextRec
:
{ Text file record structure used for Text files }
PTextBuf = ^TTextBuf;
TTextBuf = array[0..127] of AnsiChar;
TTextRec = packed record (* must match the size the compiler generates: 730 bytes (754 bytes for x64) *)
Handle: NativeInt; (* must overlay with TFileRec *)
Mode: Word;
Flags: Word;
BufSize: Cardinal;
BufPos: Cardinal;
BufEnd: Cardinal;
BufPtr: PAnsiChar;
OpenFunc: Pointer;
InOutFunc: Pointer;
FlushFunc: Pointer;
CloseFunc: Pointer;
UserData: array[1..32] of Byte;
Name: array[0..259] of WideChar;
Buffer: TTextBuf;
CodePage: Word;
MBCSLength: ShortInt;
MBCSBufPos: Byte;
case Integer of
0: (MBCSBuffer: array[0..5] of AnsiChar);
1: (UTF16Buffer: array[0..2] of WideChar);
end;
TTextIOFunc = function(var F: TTextRec): Integer;
Запись TTextRec
представляет собой внутреннюю реализацию текстовых файлов Паскаля. Как вы можете видеть, она содержит в себе указатели на функции:
OpenFunc: Pointer;
InOutFunc: Pointer;
FlushFunc: Pointer;
CloseFunc: Pointer;
Эти поля объявлены как указатели, но на самом деле трактуются как поля типа TTextIOFunc
. К сожалению, тип TTextIOFunc
нельзя объявить до TTextRec
(поскольку объявление TTextIOFunc
использует TTextRec
), поэтому тип TTextIOFunc
нельзя использовать в полях TTextRec
и приходится использовать тип Pointer
с последующим приведением типа.
В любом случае, как вы можете уже догадаться, на самом деле процедуры вроде Reset
, Rewrite
, Write
и WriteLn
не выполняют реальной работы, а лишь вызывают указанные выше процедуры через указатель — и именно эти процедуры и делают всю работу. Меняя указатели на свои, мы можем изменить поведение текстовых файлов. В этом и будет заключаться полиморфное поведение.
Вся структура TTextRec
в целом инициализируется в AssignFile
, она же заполняет и указатели на функции. Поэтому всё, что нам нужно сделать — предоставить свою реализацию каждой функции плюс аналог AssignFile
, который впишет в структуру TTextRec
наши функции, а не стандартные.
Тогда становится возможным такой код (скачать StreamText.pas):
uses
StreamText;
procedure TForm1.Button1Click(Sender: TObject);
var
Stream: TFileStream;
F: TextFile;
I: Integer;
X: Extended;
Buffer: array[0..1023] of Byte;
begin
I := 5;
X := 2.5;
// Пример #1: похоже на классические файлы Паскаля
Stream := TFileStream.Create('D:Test.txt', fmCreate, fmShareExclusive);
try
AssignStream(F, Stream); // вместо AssignFile
try
{ опционально - для оптимизации скорости } System.SetTextBuf(F, Buffer);
Rewrite(F); // = открывает с fmOutput
WriteLn(F, 'Test');
WriteLn(F, 'Value: ', I, ', Русский Текст: ', X:1:3);
finally
CloseFile(F);
end;
finally
FreeAndNil(Stream);
end;
end;
Или:
uses
StreamText;
procedure TForm1.Button2Click(Sender: TObject);
var
Stream: TFileStream;
F: TextFile;
S: String;
begin
Memo1.Lines.Clear;
// Пример #2: больше аргументов
Stream := TFileStream.Create('D:Test.txt', fmOpenRead, fmShareExclusive);
try
AssignStream(F, Stream, 1024 { опционально: размер буфера }, fmInput { опционально: режим });
try
while not EOF(F) do
begin
ReadLn(F, S);
Memo1.Lines.Add(S);
end;
finally
CloseFile(F);
end;
finally
FreeAndNil(Stream);
end;
end;
В этой статье мы рассмотрели несколько практических примеров применения полиморфизма. Мы показали использование виртуальных методов — как основного средства для реализации полиморфизма в ООП. Мы также рассмотрели события и интерфейсы — как частные случаи, призванные упростить реализацию полиморфизма для тех ситуаций, где обычное наследование с виртуальными методами даёт не самый удачный код. Наконец, мы посмотрели на возможности полиморфизма вне ООП — в рамках процедурного подхода.
В последней части серии мы рассмотрим полиморфное поведение кода без использования специальных языковых конструкций ООП или процедурного программирования.
Процесс не может получить доступ к файлу, так как этот файл занят другим процессом… Но это не точно… Как показала практика, для программы на Delphi
Из проекта
uses System.SysUtils, System.Classes, System.Generics.Collections, Rtti, xsuperjson, xsuperobject, System.TypInfo, System.Generics.Defaults; {hash map for objects on TDictionary and TList<>} THashMap<K, V> = class private FDictionary: