Показаны сообщения с ярлыком miracle. Показать все сообщения
Показаны сообщения с ярлыком miracle. Показать все сообщения

понедельник, 19 ноября 2018 г.

Список из рекордов на коленке

Честно подсмотрено.

THeader = record
  x,y,z: TMyRecordField;
  NextLabel: packed record end;
end;

На сколько прекрасно, на столько и отвратительно. Но восхищает однозначно хотя бы тем, что это вообще работает. Если что, то NextLabel это ссылка на следующию записью.

понедельник, 28 марта 2016 г.

Почему тип анонимных функций не всегда полезен или о проблема присвоения reference to типу неанонимных функций

Сегодня снова об особенностях анонимных функций.
Два предыдущих поста:
  1. Просто баг в коде или особенность счетчика ссылок в замыканиях
  2. Немного про замыкание ( Одно замыкание на несколько анонимных функций в одном контексте)

  У типа анонимный функций есть замечательная возможность. В качестве обработчика можно присваивать не только анонимные функции, но и обычные методы и статические функции. Казалось бы, нет причин почему не использовать тип reference to всегда.
Однако есть ситуации, когда reference to тип может сыграть злую шутку. И я сейчас не про вопрос производительности, о котором я думаю большинство осведомлено. 
Проблема может возникнуть при передаче обычной процедуры или метода в параметр с анонимным типом. Покажем на примере. 
Наиболее распространенный вариант использования процедурных типов в реализации подписки на события.
  Пусть есть какой-нибудь список EventHandlers, и конечно же, как у правильных хипстеров он будет параметризован типом анонимной функций TList<TProc>. 
Заведем два метода для возможности подписаться на событие и отписаться от него:  Subscribe(AHandler: TProc) и Unsubscribe(AHandler: TProc): Boolean соответственно. Однако, крайне вероятно, вас постигнет неудача если в качестве обработчика вы используете неанонимную функцию.  По крайне мере отписаться вы уже не сможете. 

Вот код примера:
program ReferenceToProc;
{$APPTYPE CONSOLE}
{$R *.res}
uses
  System.SysUtils, System.Generics.Collections;
var
  eventHandlers: TList<TProc>;

  procedure Subscribe(AHandler: TProc);
  begin
    eventHandlers.Add(AHandler);
  end;

  function Unsubscribe(AHandler: TProc): Boolean;
  begin
    Writeln(eventHandlers.IndexOf(AHandler));  // 0
    Result := eventHandlers.Remove(AHandler) >= 0;
  end;

  procedure HandlerProc;
  begin
    Writeln('Вызов статической процедуры');
  end;

var
  proc: TProc;
begin
   eventHandlers:= TList<TProc>.Create;

   proc := procedure()
     begin
       Writeln('Вызов анонимной функции')
     end;

   Subscribe(proc); // Подписываемся анонимной функцией
   Writeln(Unsubscribe(proc)); // Метод вернул True, Успех:)

   Subscribe(HandlerProc); // Подписываемся не анонимной функцией
   Writeln(Unsubscribe(HandlerProc)); // Метод вернет False Fail:(
   FreeAndNil(eventHandlers);
   readln;
end.

  Вызов Unsubscribe  в строке  Writeln(Unsubscribe(HandlerProc)); вернет False. Причина впрочем прозаична, вызов метода HandlerProc заворачивается в анонимную функцию и каждый раз новую. Потому функция, которую подписали на событие, будет отличаться от функции, которую пытаемся отписать.

  Казалось бы, простая задача - реализовать список с обработчиками и методами подписки и отписки превращается не в самую тривиальную.

  Получается, что хранить обработчики лучше как TMethod, но если все же хотим уметь подписывать универсальные обработчики (анонимные и неанонимные), тогда придется для каждого типа писать свою логику сохранения в список и вызова обработчиков. Это будут либо overload методы, либо будем использовать Rtti  и методы параметризированные типом.
  TEvent = class
    events: TList<TMethod>;
    class procedure Subscribe(AProc: TProc); overload;
    class procedure Subscribe(AProc: TProcedure); overload;
    class procedure Subscribe<T>(AProc: T); overload;
  end;

  А для преобразования анонимного метода в TMethod потребуется сочинять что то вроде того, что написано в Spring4D в Springs.Events.pas:

procedure MethodReferenceToMethodPointer(const AMethodReference; const AMethodPointer);
type
  TVtable = array[0..3] of Pointer;
  PVtable = ^TVtable;
  PPVtable = ^PVtable;
begin
  // 3 is offset of Invoke, after QI, AddRef, Release
  PMethod(@AMethodPointer).Code := PPVtable(AMethodReference)^^[3];
  PMethod(@AMethodPointer).Data := Pointer(AMethodReference);
end;

function TEvent.Cast(const handler): TMethod;
begin
  if fTypeInfo.Kind = tkInterface then
    MethodReferenceToMethodPointer(handler, Result)
  else
    Result := PMethod(@handler)^;
end;


воскресенье, 10 февраля 2013 г.

Подводные камни при работе с исключениями в Delphi: Raise Exception, AcquireExceptionObject, исключения операционной системы

    Интересно, на сколько читатель уверен в своих знаниях подводных камней в работе с исключениями в Delphi.  Я решил подготовить три наиболее часто встречаемых ошибки в  рабочем проекте. Надо признаться, некоторые моменты меня удивили. В том числе и ответ на вопрос "в какой момент удаляются объекты исключений".
    Каждый может проверить свои познания подводных камней.  В листинге ниже приведены три метода Error#, каждый из которых ведет к определенной проблеме.
Пояснения я приведу вслед за кодом.

procedure  RaiseOSException;
var
   res: integer;
   zero: integer;
begin
   zero := 0;
   res := 5 div zero;
end;

function FormatException(E:Exception): Exception;
begin
  if not (E is EMyException) then
     result := EMyException.Create(E.Message)
  else
  begin
     E.message :=  Format('Ошибка обработки исключения: %s', [E.Message]);
      result := E;
  endl
end;


procedure Error1;
begin
  try
    RaiseOSException;
  except
    on E: Exception do
    begin
      E.Message := Format('Заворачиваем в свой текст: %s', [E.Message]);
      Raise;
    end;
  end;
end;

procedure Error2;
begin
  try
    raise EMyException.Create('Alarm!!111');
  except
     on E: Exception do
     begin      
        Raise FormatException(E);
     end;
   end;
end;

procedure Error3;
begin
  try
     raise Exception.Create('Alarm!!111');;
  except
     Raise FormatException(AcquireExceptionObject);
  end;
end;

Ответы:


  • Error1 - потеря изменений в сообщение исключения;
  • Error2 - Ошибка Access Violation
  • Error3 - Утечка памяти
Разберем каждый случай более детально:

Потеря сообщений


    В методе Error1 мы изменяем текст исходного сообщения и возбуждаем исключение вновь. Но в диалоге исключения мы не увидим текста с "Заворачиваем в свой текст: %s".
    При возбуждение внешнего исключения Delphi заворачивает его в обертку - собственное исключения и дает обработать в блоке except..end.  Далее, в вызове Raise вызывается System._RaiseAgain в котором происходит удаление объекта делфового исключения(в котором мы и сделали изменение данных поля Message), а дальше возбуждается исходное внешнее исключение  которое ничего не знает про наш новый текст сообщения. 
    Более подробно можно найти описание на stackoverflow.

Ошибка Access Violation (Или "в какой момент удаляются объекты исключений")


   В методе Error2 мы поймали исключение, хотим отформатировать его текст и пробросить дальше. Однако это приведет к проблеме.  
   Проблема  случая из метода Error2 заключается в том, что Delphi попытается дважды удалить один и тот же объект исключения. Первый раз Raise FormatException(E),  второй раз скорее всего где нибудь в TApplication, в завершения цикла обработки исключений. Ответ находится в System._HandleAnyException в которой каждый сможет найти комментарий:
 { we come here if an exception handler has thrown yet another exception }
        { we need to destroy the exception object and pop the raise list. }
   Синтаксис "Raise Argument: Exception;" подразумевает что мы пытаемся возбудить другой объект исключения, а не тот, что находится сейчас на вершине стека возбужденных исключений. Для этого вычищаем объект исключения, что находится на вершине стека и вталкиваем в стек его новый объект. Проблема заключается в том, что удаляемый и вталкиваемый объект являются одним и тем же объектом. 
  Единственное место, где в документации Embarcadero явно говориться, что так делать нельзя нашел в статье Handling exceptions in Delphi. 
   Правильно выполнить вызов raise без аргументов("Raise;"), либо использовать метод AcquireExceptionObject. О нем ниже.

Утечка памяти


    В Error3 мы вместо пойманного исключения стандартного класса создаем собственное исключение, которое может более точно сообщить о проблеме при обработке в вызывающем коде. Нюанс заключается в том, что исходный  объект исключения получаем через AcquireExceptionObject. Данный метод может оказаться незаменим, например если мы хотим передать исключение из одного потока в другой. AcquireExceptionObject возлагает на нас ответственность за дальнейшее освобождения памяти полученного объекта исключения, а Delphi тем временем "умывает руки".
Справка Delphi сообщает, что за методом  AcquireExceptionObject должен следовать ReleaseExceptionObject который уменьшает счетчик ссылок на фрейм исключения(структура в rtl описывающая исключение). Получается, что  в Error3 мы забыли вызвать метод ReleaseExceptionObject? Нет. Вызов ReleaseExceptionObject нам ничем не поможет: объект  утечет в любом случае.
   В действительности счет ссылок и ReleaseExceptionObject  актуальны только для Linux. В модуле System есть объявление типа TRaiseFrame - фрейма исключений, в случае компиляции под Windows счетчик ссылок не предусмотрен. Вот текст метода:
function AcquireExceptionObject: Pointer;
begin
  if RaiseListPtr <> nil then
  begin
    Result := PRaiseFrame(RaiseListPtr)^.ExceptObject;
    PRaiseFrame(RaiseListPtr)^.ExceptObject := nil;
  end
  else
    Result := nil;
end;



Метод AcquireExceptionObject "забывает" ссылку на объект исключения во фрейме исключения. А при стандартном попытке удаление исключения  ничего не произойдет, так как предусмотрена проверка на nil в деструкторе объекта:
procedure TObject.Free;
begin
  if Self <> nil then
    Destroy;
end;

Именно по этому вызов из проблемы которая описана выше не приведет к AV:
except
  E := AcquireExceptionObject;
  Raise E; // В данном случае это правильный код
end;

А для метода test3 наиболее правильным будет решение отказаться от AcquireExceptionObject, и получать объект используя синтаксис On E: Exception do. Но все же, если удобнее использовать AcquireExceptionObject, то за ним должен следовать Raise Argument;  либо явный вызов деструктора:

procedure Error3;
var
  e : Exception;
begin
  try
     raise Exception.Create('Alarm!!111');;
  except
     e := AcquireExceptionObject;
     try
       Raise FormatException(e);
     finally
       FreeAndNil(e);
     end;
  end;
end;



Более подробно, про то, что справка иногда обманывает qc.embarcadero.com

Подводя итог, как делать нельзя:

//AV
On E: Exception do
   raise E;
//Утечка
E := AcquireExceptionObject;
raise EMyException.Create(E.Message);
//Утечка
E := AcquireExceptionObject;
try
   Foo(E);
finally
     ReleaseExceptionObject(E); //Вызов метода бессмысленнен
end;







четверг, 15 марта 2012 г.

Variant := TObject.Create ?

  Думаю многие сталкивались с тем, что иногда требуется в variant передавать blob, ну или TObject. В таких языках как Java и C# все проще, элементарные типы являются объектами ну или умеют упаковываться из значимых типов в объекты. Для всех типов в языке есть базовый тип. В Delphi же, когда тип на этапе компиляции не известен единственным универсальным решением является использование variant. 
  Но не все так гладко, тип Variant предназначен только для хранения простых типов и указатель на объект в него не поместить не используя грязных хаков. Хотя сам RTL ими так и кишит. Так для хранения ftBlob в variant используется строка. 
На самом деле есть "законный"  механизм как через Variant передавать любые данные, в том числе и объекты. Для этого есть базовый класс TCustomVariantType, на основе которого можно определить свой тип который можно будет передавать как Variant, перекрыть в нем бинарные арифметические  и логические операции. Т.е. реализовать например свое сравнению и сложение для экземпляров типа. 
Но вот тут мы подходим к самому интересному, в FireMonkey появились методы по преобразованию variant к TObject и обратно:

function VarIsObject(Value: Variant): Boolean;
function ObjectToVariant(const AObject: TObject): Variant;
function VariantToObject(const Value: Variant): TObject;

Выглядит интригующе, неправда ли? 
Но что внутри?
Совершенно обычный хак, все опять сделано через строки:

function ObjectToVariant(const AObject: TObject): Variant;
begin
  Result := 'vgobj' + IntToStr(Integer(Pointer(AObject)));
end;

function VariantToObject(const Value: Variant): TObject;
var
  S: string;
begin
  S := Value;
{$IFDEF FPCCOMP}
  if (S <> '') and (Pos(WideString('vgobj'), S) = 1) then
{$ELSE}
  if (S <> '') and (Pos('vgobj', S) = 1) then
{$ENDIF}
    Result := TObject(Pointer(StrToInt(Copy(S, 6, 10))))
  else
    Result := nil;
end;

...и расположено это все не в модули System или Variants, а в Types. Никакой поддержки на уровне компилятора нет. Т.е. все еще нельзя писать так
var 
  v: Variant;
begin
   v := TMyClass.Create();
end;

  Почему бы Embarcadero уже не реализовать нормальную поддержку объектов в variant, ну или хотя бы сделать на основе TCustomVariantType. Непонятно. Притом, что есть и наследники от него TInvokeableVariantType и TPublishableVariantType.


  Возможно, все ради совместимости с COM Variant, не хотят далеко от него уходить и пытаются сохранить Variant для типов передающихся по значению. Или, возможно, методы VariantToObject и ObjectToVariant были реализованы Евгением Крюковым KSDev, а в Embarcadero не стали ничего переделывать, работает и на том спасибо.