Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_5606.html
 

Анонимные методы в Delphi. Вопрос по отладке.

Опубликовано 30.08.2012 г. 19:02

Анонимные методы в Delphi появились достаточно давно, и в общем то не являются какой то большой темой для обсужедения, но тем не менее весьма часто могут быть полезны. Каждый сам находит им применение, иногда при работе с потоками и в других случаях.

Сегодня после внедрения в программный продукт нового функционала с анонимными методами у меня возник вопрос по их отладке, что и послужило поводом для написания этой статьи. Когда то давно я писал несколько безумных статей по поводу реализации "редактора данных". Смысл того редактора был в том, что некоторые данные выводились в таблицу, и пользователь их мог исправлять и сохранять. Основой всего был объект DataItem, предоставляющий данные о заголовках строк и столбцов (THeaderItem), вывод и измеение данных. Объект такой содержал две коллекции: заголовки строк и заголовки столбцов. Элементы этих коллекций в базовом варианте снабжались полями ID и Title. При заполнеии таблицы использовался метод getValue, получающий в виде параметров значения ID от заголовка столбца и строки данной ячейки. Поведение при редактировании/вставке/удалении было идентичным. В общем говоря эти два идентфиктора вполне определяли нужную строку из нижележащего набора данных.  Все это позволяло выводить таблицу данных и производить с ее ячейками различные операции.

Но, как это часто бывает, в таблицах с данными встречаются и некоторые агрегатные столбцы/строки, наиболее вероятный и часто встречающийся вариант - суммарная колонка или столбец. Такие суммарные данные не совсем целесообразно хранить в БД, поэтому следует их расчитывать "на лету". При этом недостаточным будет посчитать значение просто при загрузке таблице, необходимо обновлять агрегатные строки/столбцы при редактировании других ячеек. Сами агрегатные функции зависили непосредственно от типа таблицы и содержащихся в ней данных. Поэтому при вызове метода, формируещего коллекции заголовков я пополнял их новым агрегатным столбцом. Для реализации функционала агрегирования я и воспользовался анонимными функциями. Вид агрегатной функции был весьма прост. Первые два параметра - ID столбца и строки. Они не нужны, в случае если нам  необходимо определить сумму для всей строки. Но если нам необхима сумма только некоторых значений строки то мы можем отфильтровать  их по ID столбца. Аналогичная ситуация со строками. Оставшиеся два параметра - значение ячейки, и результат функции. Т.е. в общем виде сигнатура анонимного метода имела следующий вид:

    TDEAggregateProc = reference to procedure(const colId, rowId : integer; const CellValue : string; var Result : string);

значения передаются в виде строки, ибо в общем-то не известно какой тип данных используется в таблице. Поскольку сама агрегатная функция связана именно со столбцом или строкой, то описание заголовка THeaderItem было расширено, и в него были добавлены два свойства - AggregateProc - указатель на анонимный метод, и Aggregate - булевое своейство, указывающее на то, является ли столбец агрегатным. При назначении AggregateProc значение Aggregate устанавливалось в true. Таблица при выводе заполняется последовательно, например, столбцы а затем по строкам. Когда заполняя первую строку, мы встерчаем агрегатный столбец, то мы выполняем соответствующую агрегатную функцию для всей этой строки последовательно. В общем говоря, для добавления агрегатного столбца (допустим, необходимо вычислиьт сумму столбцов с ID = 1,3,7) можно было использовать примерно следующий код:

...
hi := THeaderItem.Create(100, 'Сумма');
hi.AggregateProc := procedure(const colId, rowId : integer; const CellValue : string; var Result : string)
                    var v,r : integer;
                    begin
                        if not (colId in [1,3,7]) then exit;
                        if not TryStrToInt(CellValue, v) then exit;
                        r := StrToInt(result);
                        inc(r,v);
                        Result := IntToStr(r);
                    end;
FColHeaders.Add(hi);

Данный код встраивался в процедуру TXXXDataItem.InitHeaders(). Каждыйтакой класс описывал отдельные таблицы, использовал свои собственные запросы на выборку, поэтому и агрегатные функции в общем то везде свои.

И вот такой подход к формированию агрегатных столбцов существовал какое-то время. В общем-то из всего множества классов редактирования агрегатные столбцы/строки использовались всего 2 или 3 раза. Сегодня же я решил добавить их в еще несколько таблиц. При этом сразу бросилось в глаза, что в принципе код то повторяется, и смысла в его повторении никакого нет.

Чем хороши анонимные методы, что их можно не просто определить в теле другой процедуры, но также можно и вернуть в качестве результата выполнения какой-либо функции. Поэтому весьма логичным кажется создание вспомгательной структуры, которая будет возвращать какие либо заранее определнные методы, такие как сумма строки ColumnSum() или сумма столбца RowSum(). На вход эти методы будут получать множество ID столбцов/строк, подлежащих сложению и возвращать анонимный метод с опреденной выше сигнатурой, но тело будетразличаться:

type
    TDEAggregateRange = set of byte;

    TDEAggregate = record
      public
        class function ColumnSum(colIds : TDEAggregateRange) :  TDEAggregateProc; static;
        class function RowSum(rowIds : TDEAggregateRange) : TDEAggregateProc; static;
    end;

...

class function TDEAggregate.ColumnSum(colIds: TDEAggregateRange): TDEAggregateProc;
begin
    result := procedure(const colId, rowId : integer; const CellValue : string; var Result : string)
              var v, r : double;
              begin
                  if (colIds = []) or (colId in colIDs)then begin
                      if not TryStrToFloat(cellValue, v) then exit;
                      r := StrToFloat(result);
                      r := r + v;
                      result := FloatToStr(r);
                  end;
              end;
end;

Таким образом, если нам нужна агрегатная функция суммирования значений по строке в столбца 1,2 то мы вызываем TDEAggregate.ColumnSum([1,2]), если нужны другие столбцы, то соотвественно меняем множество на нужное. В общем говоря, при каждом обращении к ColumnSum мы получаем отдельный анонимный метод, со своим множеством ColIDs.

А теперь вот собственно вопрос, который меня возник. При отладке мы можем ставить точку останова, например, внутрь этого анонимного метода, и при вызове попадем в него, после чего можем проводить отладку. В нашем случае, может формироваться достаточно большое число таких анонимных процедур суммирования сразным набором суммируемых столбцов. Т.е. поставив обычным образом точку останова, выполнение программы будет прерываться в каждый раз при вызове любой из созданных агрегатных функций. Следовательно такой вариант отладки нам не очень подходит. В принципе мы можем установить точку останова на момент вызова агрегатной функции. Как никак места, где вызываются headerItem.AggregateProc() известны. Но однако и тут бывает так, что мест таких много и т.п., что не очень удобно.

В Delphi помимо обычных точек останова (Source breakpoint), которые указаются непосредственно в коде, также существуют и другие варинаты. Например, точка останова на данные (Run -> Breakpoints -> Data Breakpoint). Таки точки мы можем использовать, когда хотим отследить все обращения какой то переменной. Для их использования необходимо запустив программу узнать адрес переменной, в момент когда она уже инициализирована, и добавить ее в DataBreakpoints. Еще одной разновидностью является Module Breakpoint, которые предназначены для прерывания выполнения программы при закгрузке различных модулей или библиотек. Последним нужным нам вариантом является - Address Breakpoint - точка останова на адрес памяти. В нашем случае адрес - точка входа в наш анонимный метод. Простой пример исходного кода, поясняющий вопрос:

program Project3;
{$APPTYPE CONSOLE}
{$R *.res}

type
  TMyProc = reference to procedure (const a, b : integer);

  TMyProcHelper = record
    public
      class function getMyProc(mult : integer):TMyProc; static;
  end;

class function TMyProcHelper.getMyProc(mult: integer): TmyProc;
begin
  result := procedure(const a,b : integer)
            begin
              writeln ('result is :', (a+b)*mult);
            end;
end;


var p1,p2 : TMyProc;

const x = 1;
      y = 1;

begin
  try
    p1 := TMyProcHelper.getMyProc(-1);
    p2 := TMyProcHelper.getMyProc(10);

    p1(x,y);
    p2(x,y);

    readln;
  except
    writeln('fail');
  end;
end.

Соответственно анонимный метод связанный с переменной p1 складывает аргументы (1+1) и умножает на -1. Второй из p2 опять таки складывает, но умножает на 10. Ситуация - я хочу отладить выолпнение метода p2(). сами переменные p1 и p2 содержат адреса анонимных методов. В общем у меня есть два варианта: поставить точку останова на строку вызова p2(), либо установить точку останова на адрес вызова p2. С первым проблем никаких нет, но допустим нам этот вариант не удобен. Вот собственно и вопрос - как установить точку останова на адрес вызова p2()? Все мои попытки сделать это приводили к AV.

 

Метки:  debug  |  anonymous methods 

Комментарии

Александр
30.08.2012 в 21:36
p1 и p2, на самом деле, - интерфейсы. А вызов p1 и p2 - это вызов первого интерфейсного метода (первого - не считая служебных). Соответственно, там стоит call[ptr], где указатель указывает на заглушку интерфейса. Как я понял, тебе на неё и нужно поставить точку останова.

Делается это так: ставь бряк на строчку result := твоя-функа (или p1 := ...). Пусть она выполнится. Потом вычисляй PPointer(PByte(PPointer(Result)^) + 4 * 3)^ (замени Result на p1/p2 при необходимости).

Здесь: PPointer(Result)^ - указатель на VMT, 4 * 3 - пропускаем три метода (QueryInterface, AddRef, Release), а внешний PPointer - это мы берём указатель на заглушку из VMT.

Копируешь результат вычисления в буфер, потом открываешь CPU отладчик, правой кнопкой по полю кода - Go to address, вводи значение из буфера (вместе с ведущим $) и OK.

Тебя должно кинуть на инструкцию add eax, -$10 - вот на неё ставь бряк.
teran
30.08.2012 в 22:44
да, действительно где-то читал про интерфейсы.

дак а в Address breakpoint если адрес этот поставить не то же самое то будет?
зы: почему/откуда PByte
зы2: 4*3 по идее должно быть sizeof(pointer)*3 для совместимости с х64 ?
Александр
30.08.2012 в 22:03
Да, можно Add address breakpoint. Я про CPU сказал, чтобы проверить, что ты нашёл нужное место.

PByte нужен для адресной арифметики. Можно ещё PAnsiChar.

4 = SizeOf(Pointer), да.
teran
30.08.2012 в 23:03
ну то что для арифметики мне понятно. не понятно почему PByte, если адрес состоит из 4х байт, почему не к int/NativeInt приводить надо?

в прочем проверил, вариант с PByte не работает.

а по формуле PPointer(PInteger(PPointer(@p2)^)^ + 3*sizeof(integer))^
получил вроде корректный результат
teran
31.08.2012 в 00:50
данный адрес кстати попадет не во внутренний begin анонимной функции и выполняется для обоих анонимных методов с -1 и 10.
Александр
09.09.2012 в 10:10
Возможно я что-то не так понял.

Анонимные методы параметризируются, а не дублируются кодом. Т.е. два анонимных метода (с одного кода) - это два одинаковых объекта с разными значениями полей, но не разный код.
Torbins
11.09.2012 в 14:57
А на поля этого объекта можно поставить дата-бряк?
Александр
30.08.2012 в 21:39

Вообще, тебе может пригодится такой подход:

procedure DebugBreak;
asm
  int 3;
end;

class function TMyProcHelper.getMyProc(mult: integer): TmyProc;
begin
  result := procedure(const a,b : integer)
            begin
              if mult = -1 then
                DebugBreak;
              writeln ('result is :', (a+b)*mult);
            end;
end;

It's difficult to find educated people about
this topic, but you sound like you know what
you're talking about! Thanks
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно