Как известно, в языке есть такие сущности как процедуры/функции и методы. Причём начинающие программисты часто путают эти два понятия.
Функция (в дальнейшем здесь будет также подразумеваться и процедура) - это код. Процедурная переменная - это указатель на код. Например:
type TDoSomethingFunc = function(const P1, P2, P3: Integer): Integer; function DoSomething(const A, B, C: Integer): Integer; begin Result := (A + B) * C; end; ... var Func: TDoSomethingFunc; I: Integer; ... Func := DoSomething; I := Func(1, 2, 3); ShowMessage(IntToStr(I));Метод - это тоже код, но код, связанный с классом. Указатель на метод - это ссылка на код + ссылка на конкретный объект. Например:
type TDoSomethingFunc = function(const P1, P2, P3: Integer): Integer of object; type TSomeObj = class function DoSomething(const A, B, C: Integer): Integer; end; function TSomeObj.DoSomething(const A, B, C: Integer): Integer; begin Result := (A + B) * C; end; ... var Func: TDoSomethingFunc; Obj: TSomeObj; I: Integer; ... Obj := TSomeObj.Create; try Func := Obj.DoSomething; I := Func(1, 2, 3); finally Obj.Free; end; ShowMessage(IntToStr(I));Когда путают одно с другим компилятор чаще всего показывает такое сообщение: "Incompatible types: regular procedure and method pointer". Чаще всего или забывают писать "of object" в объявлении своих процедурных типов или пытаются передать в функцию (чаще всего как callback - т.е. функцию обратного вызова) метод класса вместо обычной функции (а самым упорным это иногда удаётся).
Что делает эти две сущности такими принципиально несовместимыми? Функция - это просто код. Она не имеет связи с данными, отличными от тех, что передаются в её параметры. Методы класса помимо работы с параметрами (как и обычная функция) ещё могут оперировать с данными объекта (вот оно: "код" vs "код + данные"), например:
type TDoSomethingFunc = function(const P1, P2: Integer): Integer of object; type TSomeObj = class P3: Integer; function DoSomething(const A, B: Integer): Integer; end; function TSomeObj.DoSomething(const A, B: Integer): Integer; begin Result := (A + B) * P3; end; ... var Func: TDoSomethingFunc; Obj: TSomeObj; I: Integer; ... Obj := TSomeObj.Create; try Obj.P3 := 3; Func := Obj.DoSomething; I := Func(1, 2); finally Obj.Free; end; ShowMessage(IntToStr(I));С функциями такое невозможно - обратите внимание, как вы манипулируете с P3 (он же: Self.P3) в методе. Собственно сам объект (это встроенная переменная Self) неявно передаётся в метод первым параметром. Поэтому, если метод объявлен как
function(const P1, P2: Integer): Integer of object
- с двумя параметрами, то, на самом деле, он трактуется как функция с тремя параметрами: function(Self: TSomeObj; const P1, P2: Integer): Integer
. Именно это различие (на бинарном уровне) делает несовместимыми обычные функции и методы.Соответственно, указатель на обычную функцию - это просто указатель (pointer), только что типизированный (это я про TDoSomethingFunc) - т.е. 4 байта. А вот указатель на метод - это уже запись или, если будет угодно, два указателя - один на код, второй - на данные, т.е. всего 8 байт.
Понятно, что если у вас что-то предназначено для передачи указателя на функцию, то никакими силами указатель на метод вы туда не пропихнёте. Хотя бы потому, что размер у них разный.
Ещё в Delphi есть классовые методы. Это такие методы, которые можно вызывать не имея на руках объект. В этом случае вместо объекта в неявный параметр Self передаётся информация о классе. Т.е. в классовых методах вы не можете использовать информацию о конкретном объекте (например, читать/писать его поля), но можете использовать информацию о классе - например, вызывать конструктор класса. Также методы класса могут быть виртуальными. Заметим, что сигнатура функции, реализующей метод, всё ещё совпадает с сигнатурой обычного метода: неявный параметр (данные класса вместо Self) + все явные параметры метода.
Например:
type TDoSomethingFunc = function(const P1, P2: Integer): Integer of object; type TSomeObj = class P3: Integer; class function DoSomething(const A, B: Integer): Integer; end; class function TSomeObj.DoSomething(const A, B: Integer): Integer; begin Result := (A + B); // поскольку P3 имеет смысл только при наличии конкретного объекта, то здесь его использовать не получится end; ... var Func: TDoSomethingFunc; I: Integer; ... // Можно использовать классовый метод без экземпляра: Func := TSomeObj.DoSomething; I := Func(1, 2); ShowMessage(IntToStr(I));Теперь ещё один шажок и мы переходим к тому, о чём говорил Реймонд Чен. Классовый метод можно объявить статическим (только в новых версиях Delphi). В этом случае у него не будет неявного параметра. Разумеется, при этом он не может использовать информацию экземпляра и класса. Зато он и не отличается от обычной функции.
Рассматривая пример с потоком, вот что мы могли бы написать в старых Delphi без поддержки статических классовых методов:
type TMyThread = class private FHandle: THandle; FID: Cardinal; function Execute: DWord; public constructor Create; destructor Destroy; override; end; // Вспомогательная функция, не принадлежащая классу function ThreadProc(Param: Pointer): DWord; stdcall; begin TMyThread(Param).Execute; end; { TMyThread } constructor TMyThread.Create; begin IsMultiThread := True; FHandle := CreateThread(nil, 0, @ThreadProc, Self, 0, FID); end; destructor TMyThread.Destroy; begin CloseHandle(FHandle); FHandle := 0; FID := 0; inherited; end; function TMyThread.Execute: DWord; begin MessageBox(0, 'Hello from thread', 'Information', MB_OK or MB_ICONINFORMATION); Result := 0; end;Теперь, с введением нового ключевого слова static, появилась возможность писать так:
type TMyThread = class private FHandle: THandle; FID: Cardinal; class function ThreadProc(Param: Pointer): DWord; stdcall; static; function Execute: DWord; public constructor Create; destructor Destroy; override; end; { TMyThread } constructor TMyThread.Create; begin IsMultiThread := True; FHandle := CreateThread(nil, 0, @ThreadProc, Self, 0, FID); end; destructor TMyThread.Destroy; begin CloseHandle(FHandle); FHandle := 0; FID := 0; inherited; end; class function TMyThread.ThreadProc(Param: Pointer): DWord; begin Result := TMyThread(Param).Execute; end; function TMyThread.Execute: DWord; begin MessageBox(0, 'Hello from thread', 'Information', MB_OK or MB_ICONINFORMATION); Result := 0; end;При этом Реймонд говорит о том, что если у Execute сделать модель вызова stdcall, то бинарные сигнатуры параметра CreateThread, методов ThreadProc и Execute совпадут - поэтому, мол, умный компилятор уменьшит код ThreadProc до простого jmp. Увы, но компилятор Delphi не настолько умён - в этом случае он генерирует полный вызов вместе с передачей параметра.
Разумеется, можно сделать и так (обратите внимание на stdcall):
type TMyThread = class private FHandle: THandle; FID: Cardinal; function Execute: DWord; stdcall; public constructor Create; destructor Destroy; override; end; { TMyThread } constructor TMyThread.Create; begin IsMultiThread := True; FHandle := CreateThread(nil, 0, @TMyThread.Execute, Self, 0, FID); end; destructor TMyThread.Destroy; begin CloseHandle(FHandle); FHandle := 0; FID := 0; inherited; end; function TMyThread.Execute: DWord; begin MessageBox(0, 'Hello from thread', 'Information', MB_OK or MB_ICONINFORMATION); Result := 0; end;Но, как было сказано, это не рекомендуется, т.к. здесь во-первых, есть шанс сильно напутать (так можно делать только при условии полной бинарной совместимости, что будет далеко не всегда - ключевой момент здесь: передача контекста именно первым параметром), кроме того, последний вариант опирается на тот факт, что параметр у CreateThread "случайно" совпал с сигнатурой TMyThread.Execute - это называется использование деталей реализации.
Ещё ссылки для дальнейшего чтения:
- Перевод справки Delphi.
- Основы работы с Win API в VCL-приложениях.
- Про локальные функции.
- Методы vs функции, классовые vs статик методы и всё остальное.
Ну а какие есть тогда гарантии, что сигнатура у static-варианта ThreadProc совпадёт с параметром в CreateProcess?
ОтветитьУдалитьа как статические классовые методы обьявить в старых версиях дельфи?
ОтветитьУдалить>>> Ну а какие есть тогда гарантии, что сигнатура у static-варианта ThreadProc совпадёт с параметром в CreateProcess?
ОтветитьУдалитьНу вообще-то это хороший вопрос :)
Это сказано в справке Delphi следующим образом:
>>> Like class methods, class static methods
>>> can be accessed without an object reference.
>>> Unlike ordinary class methods, class static
>>> methods have no Self parameter at all. They
>>> also cannot access any instance members
>>> (they still have access to class fields,
>>> class properties, and class methods).
>>> Also unlike class methods, class static
>>> methods cannot be declared virtual.
>>> а как статические классовые методы обьявить в старых версиях дельфи?
А никак. Их же там нет.
Последнюю ссылку ещё посмотрите - там все слова уже сказаны.
странно там как раз пишут ". Смотри, классовый метод в Delphi действительно работает с классом. А статик-метод в Java - нет."
ОтветитьУдалитькак в дельфе классовый метод работает с классом?
>>> как в дельфе классовый метод работает с классом?
ОтветитьУдалитьКлассовый метод и статический классовый метод - это _разные_ вещи.
Как я уже сказал, у статического классового метода (который появился только недавно) нет никаких скрытых параметров - только те, что явно описаны при его объявлении. Соответственно, статический классовый метод вообще с классом никак не связан.
Обычный классовый метод (который есть во всех версиях Delphi) имеет неявный скрытый параметр, в котором передаётся указатель на класс (в отличие от указателя на объект в обычных не классовых методах).
При этом классовый метод может использовать информацию о классе - например, это виртуальные функции, в том числе конструкторы. Посмотрите хэлп Delphi - там есть несколько примеров.
P.S. Про Яву я ничего не знаю ;)
ОтветитьУдалитьда про обычные методы классов то все как раз понятно ;)
ОтветитьУдалитьтам по любому идет поинтер на экзепляр структуры данных. все ясно путаница в терминологии короче вышла :)
я думал что статик метод = в дельфе классовый метод
ну типа там при определении класса как то дополнительно задается.
Спасибо, тоже делал класс для потока про static не знал. Подскажите можно ли использовать функцию WriteFile в разных потоках при записи в один файл но в разные не пересекающиеся части ? Увеличит ли это производительность, винт все равно последовательно пишет ?
ОтветитьУдалитьЯ пытаюсь использовать стaтический метод для SetTimer:
ОтветитьУдалитьtype
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
class procedure cb (Handle:HWND;uMsg:DWORD;idEvent:DWORD;dwTime:DWORD); stdcall; static;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
begin
SetTimer(form1.Handle, 1, 2000, @cb);
//здесь выдает ошибку [Pascal Error] Unit1.pas(35): E2036 Variable required
end;
class procedure TForm1.cb(Handle: HWND; uMsg, idEvent, dwTime: DWORD); stdcall;
begin
if idEvent = 1 then
showmessage('1');
end;
не понимаю где здесь ошибка
Delphi XE - полёт нормальный.
ОтветитьУдалитьВаша проблема: QC Report #38866.
Хорошо. Допустим, есть класс (TCP-сервер с перекрытым вводом-выводом на основе процедур завершения), в котором используются процедуры завершения, не являющиеся методами класса (подставляются в lpCompletionRoutine, например, в WSARecv(). Это понятно. Но в самих процедурах завершения должны вызываться методы класса (что-то вроде RemoveConnection). Как тут быть?
ОтветитьУдалитьСм. последние два примера в заметке + это:
ОтветитьУдалитьCompletionKey
The per-handle user-defined completion key that is included in every I/O completion packet for the specified file handle.
Use the CompletionKey parameter to help your application track which I/O operations have completed. This value is not used by CreateIoCompletionPort for functional control; rather, it is attached to the file handle specified in the FileHandle parameter at the time of association with an I/O completion port. This completion key should be unique for each file handle, and it accompanies the file handle throughout the internal completion queuing process. It is returned in the GetQueuedCompletionStatus function call when a completion packet arrives. The CompletionKey parameter is also used by the PostQueuedCompletionStatus function to queue your own special-purpose completion packets.
Т.е по аналогии в качестве процедур завершения использовать методы класса и в функции, аналогичные WSARecv, WSASend, в качестве CompletionRoutine подставлять указатель на метод класса?
ОтветитьУдалить>>> в качестве CompletionRoutine подставлять указатель на метод класса?
ОтветитьУдалитьНет. Почему вдруг на метод? Посмотрите пример внимательнее. На объект.
>>> в качестве процедур завершения использовать методы класса и в функции, аналогичные WSARecv, WSASend
Будете ли вы использовать классовые статические методы или же обыкновенные функции - не имеет значения. Главное - передавать объект в user-параметре.
Не так давно программирую, потому не все еще понятно... Могу я Вам код класса отправить и вызывающей формы?
ОтветитьУдалитьОбратитесь лучше на форумы, у меня сейчас нет свободного времени.
ОтветитьУдалитьНемного разобрался. Процедуры завершения не принадлежат классу TTcpServer, в конце блока interface var Obj: TTcpServer, при вызове WSARecv(ClientSocket, @Buf, 1, NumBytes, Flags, @Overlapped, @ReadMsgCompleted), где ReadMsgCompleted - процедура завершения...
ОтветитьУдалитьПри вызове методов класса Obj.blablabla()
ОтветитьУдалитьinterface
ОтветитьУдалить...
var
Obj: TTcpServer;
...
implementation
procedure ReadLenCompleted(dwError: DWORD; cdTransferred: DWORD; lpOverlapped: PWSAOverlapped; dwFlags: DWORD); stdcall;
begin
...
if WSARecv(ClientSocket, @Buf, 1, NumBytes, Flags, @Overlapped, @ReadMsgCompleted) = SOCKET_ERROR then
begin
...
end;