My current code looks like this:
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Winapi.Windows,
System.Generics.Collections,
System.SysUtils;
type
TForm1 = class
public
Events: TList<TProc>;
constructor Create;
destructor Destroy; override;
end;
TTracingInterfacedObject = class(TInterfacedObject)
public
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
ISharedPtr<T> = interface
['{CC9EE6C5-F07B-40E5-B05D-2DFDBD3404A1}']
function Get: T;
function GetRefCount: Integer;
end;
ICatalog = interface
['{F421BBA8-8DA3-47EE-ADB9-DED26747472E}']
function GetView: ISharedPtr<TForm1>;
property View: ISharedPtr<TForm1> read GetView;
end;
ITree = interface
['{A1E2F71B-124B-48DB-B038-5F90AC5BE94B}']
function GetId: TGUID;
property Id: TGUID read GetId;
end;
TSharedPtr<T: class> = class(TTracingInterfacedObject, ISharedPtr<T>)
private
FObject: T;
public
constructor Create(const AObject: T);
destructor Destroy; override;
function GetRefCount: Integer;
function Get: T;
end;
TCatalog = class(TTracingInterfacedObject, ICatalog)
private
FView: ISharedPtr<TForm1>;
public
constructor Create;
function GetView: ISharedPtr<TForm1>;
end;
TTree = class(TTracingInterfacedObject, ITree)
private
FView: ISharedPtr<TForm1>;
public
constructor Create(const AView: ISharedPtr<TForm1>);
function GetId: TGUID;
end;
function TTracingInterfacedObject._AddRef: Integer;
begin
OutputDebugString(PChar(ClassName + '._AddRef'));
Result := inherited _AddRef;
end;
function TTracingInterfacedObject._Release: Integer;
begin
OutputDebugString(PChar(ClassName + '._Release'));
Result := inherited _Release;
end;
constructor TForm1.Create;
begin
inherited;
Events := TList<TProc>.Create;
end;
destructor TForm1.Destroy;
begin
Events.Free;
inherited;
end;
constructor TSharedPtr<T>.Create(const AObject: T);
begin
inherited Create;
FObject := AObject;
end;
destructor TSharedPtr<T>.Destroy;
begin
FObject.Free;
inherited;
end;
function TSharedPtr<T>.Get: T;
begin
Result := FObject;
end;
function TSharedPtr<T>.GetRefCount: Integer;
begin
Result := FRefCount;
end;
constructor TCatalog.Create;
begin
inherited Create;
FView := TSharedPtr<TForm1>.Create(TForm1.Create) as ISharedPtr<TForm1>;
end;
function TCatalog.GetView: ISharedPtr<TForm1>;
begin
Result := FView;
end;
constructor TTree.Create(const AView: ISharedPtr<TForm1>);
begin
inherited Create;
FView := AView;
end;
function TTree.GetId: TGUID;
begin
Result := TGUID.Empty;
end;
procedure Main;
var
Catalog: ICatalog;
Tree: ITree;
Func: TFunc<TGUID>;
Events: TList<TProc>;
Event: TProc;
begin
Catalog := TCatalog.Create as ICatalog;
Events := Catalog.View.Get.Events;
Event := procedure
begin
end;
Events.Add(Event);
Tree := TTree.Create(Catalog.View) as ITree;
Func := function: TGUID
begin
Result := Tree.Id;
end;
end;
begin
Main;
end.
I have set a breakpoint at the final end.
of the application.
The event log looks like this at that point:
Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TCatalog._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._AddRef Process Project1.exe (3456)
Debug Output: TTree._AddRef Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._Release Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._Release Process Project1.exe (3456)
Debug Output: TCatalog._Release Process Project1.exe (3456)
Debug Output: TSharedPtr<Project1.TForm1>._Release Process Project1.exe (3456)
Source Breakpoint at $0047F675: C:\Users\Admin\Documents\Embarcadero\Studio\Projects\ViewFail\Project1.dpr line 168. Process Project1.exe (3456)
So:
Why does this happen? Is there a reference cycle somewhere that I am missing?
Yes, there is a reference cycle in your code. It is created through anonymous method variable capture mechanism.
Anonymous methods are backed by reference counted, compiler generated class. Any variables captured by anonymous method are stored as fields in the same class. Compiler instantiates instance of that class and keeps it alive as long as anonymous method is in scope.
Now, above facts would not be enough to create cycle. But same instance (same class) will be used to back up all anonymous methods within some routine.
Translated to your code:
TForm1
holds Events
Catalog
holds TForm1
Tree
holds TForm1
No cycles there - Tree
does not reference Catalog
, nor Catalog
references Tree
But, when you look at your Main
procedure things change.
Anonymous methods in Main
will be backed by hidden object instance - so let's see what will be there:
Tree
- captured by second anonymous method Still no visible cycles - but, then you add first anonymous method Event
to the Events
list that is held by Tree
. In order to keep that method alive the whole backing object will be kept alive too.
Anonymous method object -> Event
-> Tree -> Events -> Event -> Anonymous method object
To break that cycle you have to clear some references. For instance set Tree
to nil
somewhere in Main
.