Реализация Smart Pointers в Delphi, и еще немного
В последних версиях 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
Осталось добавить немного синтаксического сахара, с явным и не явным созданием нашего интерфейса:
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
, а не вызывать функцию 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;
Надеюсь, кому то это окажется полезно.