Реализация Smart Pointers в Delphi, и еще немного

9fce8d88cdcc79252ca5446e4c2bce9d

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

Для начала приведу пример двух процедур, первая классическая, вторая использует новые типы. Далее рассмотрим как это реализовано.

procedure ClassicVersion;
var
  Reader, Writer: TFileStream;
begin
  Reader := TFileStream.Create('C:\hiberfil.sys', fmOpenRead);
  try
    Writer := TFileStream.Create('D:\dummy.sys', fmCreate);
    try
      // Делаем что-то полезное
    finally
      Writer.Free;
    end;
  finally
    Reader.Free;
  end;
end;

procedure ARCVersion;
var
  Reader, Writer: AutoRef;
begin
  Reader := Ref(TFileStream.Create('C:\hiberfil.sys', fmOpenRead));
  Writer := Ref(TFileStream.Create('D:\dummy.sys', fmCreate));
  // Делаем что-то полезное
end;

В процедуре ClassicVersion, используется обычный подход, с портянкой из try … finally для гарантированного освобождения ресурсов. Вторая содержит всего две строки, создания объектов, все остальное (освобождение объектов) происходит автоматически. Разница очевидна, две строки кода, вместо девяти. Код короче и нагляднее. Я думаю для многих Delphi разработчиков это выглядит как немного магии. Давайте рассмотрим, как это работает.

type
  AutoRef = reference to function: T;

Выглядит как объявление ссылки на функцию — так и есть. Самое интересное — реализация в Delphi, оказывается это не только ссылка, но и интерфейс! Для меня, например, это была новость. Если это интерфейс, значит: он реализует Automatic Reference Counting (ARC) и мы можем создать класс, который его реализует:

TAutoRef = class(TInterfacedObject, AutoRef)
protected
  function Invoke: T;
end;

Обратите внимание, в интерфейсе должен присутствовать метод Invoke повторяющий сигнатуру нашей функции. Попробуйте — это работает! По сути осталось немного, добавить владение объектом:

TAutoRef = class(TInterfacedObject, AutoRef)
private
  FValue: T;
protected
  function  Invoke: T;
public
  constructor Create(const Value: T);
  destructor Destroy; override;
end;

И так, у нас есть класс, который ведет себя как функция, т.е. возвращает наш объект, и мы можем обращаться к его членам. И который, при этом, реализует интерфейс AutoRef, т.е. реализует ARC, и соответственно в деструкторе освобождение объекта .

Осталось добавить немного синтаксического сахара, с явным и не явным созданием нашего интерфейса:

Ref = record
private type
  TAutoRef = class(TInterfacedObject, AutoRef)
  private
    FValue: T;
  protected
    function  Invoke: T;
  public
    constructor Create(const Value: T);
    destructor Destroy; override;
  end;
private
  FValue: T;
public
  class function Create(const Value: T): AutoRef; static; inline;
  class operator Implicit(const Value: Ref): AutoRef; static; inline;
  class operator Explicit(const Value: T): Ref; static; inline;
end;

Реализация, этих типов, тривиальна, но в конце статьи я приведу полный текст Unit’а с реализацией. Интересно наличие двух перегруженных операторов явного и неявного приведения типов, они позволяют код, к в первом примере: MyRef := Ref(MyObj);, а не вызывать функцию Create.

Если мы хотим освободить объект (ну или по крайней мере уменьшить счетчик ссылок), все просто, мы нашему объекту присваиваем nil. Когда в программе не останется ссылок на объект, он будет автоматически освобожден.

Маленький бонус, создадим тип DeferredRef, который возвращает тот же интерфейс, но создает его только при первом обращении к нему, для этого, ему в качестве параметра передается тот-же AutoRef, который вызывается при создании объекта.

DeferredRef = record
private type
  TDeferredRef = class(TInterfacedObject, AutoRef)
  private
    FCreator: AutoRef;
    FValue: T;
  protected
    function  Invoke: T;
  public
    constructor Create(const Creator: AutoRef);
    destructor Destroy; override;
  end;
private
  FCreator: AutoRef;
public
  class function Create(const Creator: AutoRef): AutoRef; static; inline;
  class operator Implicit(const Value: DefferedRef): AutoRef; static; inline;
  class operator Explicit(const Value: AutoRef): DefferedRef; static; inline;
end;

Честно говоря, в реальной жизни, подобное, отложенное создание не часто нужно, ну по крайней мере мне. Но подобные задачи встречались при при разработке многопоточных систем, поэтому создание объекта реализовано Thread Safe.

Как обещал, полный исходный текст:

unit Primitives;

interface

uses
  System.Types, System.SysUtils;

type
  ///
  ///  Because of Delphi realisation of anonymous methods,
  ///  this type can be treated not only as Delegate,
  ///  but also as interface with Invoke method that returns generics type 
  ///
  AutoRef = reference to function: T;

  Shared = class
  private
    class var FLock: TObject;
  protected
    class constructor Create;
    class destructor Destroy;
  public
    class procedure Initialize(var Value: T; const Initializer: AutoRef); static; inline;
  end;

  ///
  ///  This type realizes creating ARC owner of object instance type that freed
  ///  owned object when out of scope (reference count = 0)
  ///
  Ref = record
  private type
    TAutoRef = class(TInterfacedObject, AutoRef)
    private
      FValue: T;
    protected
      function  Invoke: T;
    public
      constructor Create(const Value: T);
      destructor Destroy; override;
    end;
  private
    FValue: T;
  public
    class function Create(const Value: T): AutoRef; static; inline;
    class operator Implicit(const Value: Ref): AutoRef; static; inline;
    class operator Explicit(const Value: T): Ref; static; inline;
  end;

  ///
  ///  This type same as Ref, but with deferred creation of owned object.
  ///  Also, this type is thread safe.
  ///
  DeferredRef = record
  private type
    TDeferredRef = class(TInterfacedObject, AutoRef)
    private
      FCreator: AutoRef;
      FValue: T;
    protected
      function  Invoke: T;
    public
      constructor Create(const Creator: AutoRef);
      destructor Destroy; override;
    end;
  private
    FCreator: AutoRef;
  public
    class function Create(const Creator: AutoRef): AutoRef; static; inline;
    class operator Implicit(const Value: DeferredRef): AutoRef; static; inline;
    class operator Explicit(const Value: AutoRef): DeferredRef; static; inline;
  end;

implementation

{ Shared }

class constructor Shared.Create;
begin
  FLock := TObject.Create;
end;

class destructor Shared.Destroy;
begin
  FreeAndNil(FLock);
end;

class procedure Shared.Initialize(var Value: T; const Initializer: AutoRef);
begin
  if not Assigned(Value) then
  begin
    System.TMonitor.Enter(FLock);
    try
      if not Assigned(Value) then
        Value := Initializer();
    finally
      System.TMonitor.Exit(FLock);
    end;
  end;
end;

{ Ref.TAutoRef }

constructor Ref.TAutoRef.Create(const Value: T);
begin
  FValue := Value;
end;

destructor Ref.TAutoRef.Destroy;
begin
  FreeAndNil(FValue);
end;

function Ref.TAutoRef.Invoke: T;
begin
  Result := FValue;
end;

{ Ref }

class function Ref.Create(const Value: T): AutoRef;
begin
  Result := TAutoRef.Create(Value);
end;

class operator Ref.Implicit(const Value: Ref): AutoRef;
begin
  Result := TAutoRef.Create(Value.FValue);
end;

class operator Ref.Explicit(const Value: T): Ref;
begin
  Result.FValue := Value;
end;

{ DeferredRef }

class function DeferredRef.Create(const Creator: AutoRef): AutoRef;
begin
  Result := TDeferredRef.Create(Creator);
end;

class operator DeferredRef.Explicit(const Value: AutoRef): DeferredRef;
begin
  Result.FCreator := Value;
end;

class operator DeferredRef.Implicit(const Value: DeferredRef): AutoRef;
begin
  Result := TDeferredRef.Create(Value.FCreator);
end;

{ DeferredRef.TDeferredRef }

constructor DeferredRef.TDeferredRef.Create(const Creator: AutoRef);
begin
  FCreator := Creator;
  FValue := nil;
end;

destructor DeferredRef.TDeferredRef.Destroy;
begin
  FreeAndNil(FValue);
end;

function DeferredRef.TDeferredRef.Invoke: T;
begin
  Shared.Initialize(FValue, FCreator);
  Result := FValue;
end;

Надеюсь, кому то это окажется полезно.

© Habrahabr.ru