22 декабря 2008 г.

Новое ключевое слово static в Delphi

Недавно я переводил пост Почему методы класса должны быть помечены словом "static", чтобы их можно было использовать в качестве функции обратного вызова? Реймонда Чена. Там я оставил весь код "как есть" - на C++. Здесь я рассмотрю этот вопрос с точки зрения Delphi.

Как известно, в языке есть такие сущности как процедуры/функции и методы. Причём начинающие программисты часто путают эти два понятия.

Функция (в дальнейшем здесь будет также подразумеваться и процедура) - это код. Процедурная переменная - это указатель на код. Например:
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 статик методы и всё остальное.

19 комментариев :

  1. Ну а какие есть тогда гарантии, что сигнатура у static-варианта ThreadProc совпадёт с параметром в CreateProcess?

    ОтветитьУдалить
  2. а как статические классовые методы обьявить в старых версиях дельфи?

    ОтветитьУдалить
  3. >>> Ну а какие есть тогда гарантии, что сигнатура у 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.

    >>> а как статические классовые методы обьявить в старых версиях дельфи?
    А никак. Их же там нет.
    Последнюю ссылку ещё посмотрите - там все слова уже сказаны.

    ОтветитьУдалить
  4. странно там как раз пишут ". Смотри, классовый метод в Delphi действительно работает с классом. А статик-метод в Java - нет."
    как в дельфе классовый метод работает с классом?

    ОтветитьУдалить
  5. >>> как в дельфе классовый метод работает с классом?
    Классовый метод и статический классовый метод - это _разные_ вещи.
    Как я уже сказал, у статического классового метода (который появился только недавно) нет никаких скрытых параметров - только те, что явно описаны при его объявлении. Соответственно, статический классовый метод вообще с классом никак не связан.
    Обычный классовый метод (который есть во всех версиях Delphi) имеет неявный скрытый параметр, в котором передаётся указатель на класс (в отличие от указателя на объект в обычных не классовых методах).
    При этом классовый метод может использовать информацию о классе - например, это виртуальные функции, в том числе конструкторы. Посмотрите хэлп Delphi - там есть несколько примеров.

    ОтветитьУдалить
  6. P.S. Про Яву я ничего не знаю ;)

    ОтветитьУдалить
  7. да про обычные методы классов то все как раз понятно ;)
    там по любому идет поинтер на экзепляр структуры данных. все ясно путаница в терминологии короче вышла :)
    я думал что статик метод = в дельфе классовый метод
    ну типа там при определении класса как то дополнительно задается.

    ОтветитьУдалить
  8. Спасибо, тоже делал класс для потока про static не знал. Подскажите можно ли использовать функцию WriteFile в разных потоках при записи в один файл но в разные не пересекающиеся части ? Увеличит ли это производительность, винт все равно последовательно пишет ?

    ОтветитьУдалить
  9. Я пытаюсь использовать ст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;

    не понимаю где здесь ошибка

    ОтветитьУдалить
  10. Delphi XE - полёт нормальный.

    Ваша проблема: QC Report #38866.

    ОтветитьУдалить
  11. Хорошо. Допустим, есть класс (TCP-сервер с перекрытым вводом-выводом на основе процедур завершения), в котором используются процедуры завершения, не являющиеся методами класса (подставляются в lpCompletionRoutine, например, в WSARecv(). Это понятно. Но в самих процедурах завершения должны вызываться методы класса (что-то вроде RemoveConnection). Как тут быть?

    ОтветитьУдалить
  12. См. последние два примера в заметке + это:

    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.

    ОтветитьУдалить
  13. Т.е по аналогии в качестве процедур завершения использовать методы класса и в функции, аналогичные WSARecv, WSASend, в качестве CompletionRoutine подставлять указатель на метод класса?

    ОтветитьУдалить
  14. >>> в качестве CompletionRoutine подставлять указатель на метод класса?

    Нет. Почему вдруг на метод? Посмотрите пример внимательнее. На объект.

    >>> в качестве процедур завершения использовать методы класса и в функции, аналогичные WSARecv, WSASend

    Будете ли вы использовать классовые статические методы или же обыкновенные функции - не имеет значения. Главное - передавать объект в user-параметре.

    ОтветитьУдалить
  15. Не так давно программирую, потому не все еще понятно... Могу я Вам код класса отправить и вызывающей формы?

    ОтветитьУдалить
  16. Обратитесь лучше на форумы, у меня сейчас нет свободного времени.

    ОтветитьУдалить
  17. Немного разобрался. Процедуры завершения не принадлежат классу TTcpServer, в конце блока interface var Obj: TTcpServer, при вызове WSARecv(ClientSocket, @Buf, 1, NumBytes, Flags, @Overlapped, @ReadMsgCompleted), где ReadMsgCompleted - процедура завершения...

    ОтветитьУдалить
  18. При вызове методов класса Obj.blablabla()

    ОтветитьУдалить
  19. 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;

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.