After being away from Delphi for a while (and quite happily programming in VB.NET),
I returned to one of my older projects (YAC Text Recoder) to implement filters
(basically, simple expressions using comparison and arithmetic operators on numeric and text values,
that are then used to select a subset of the data - think of WHERE clauses in SQL).
To check that the expression is being parsed correctly, I wanted to add a method that displays
the parse tree one element per line, with indenting showing the depth of the tree at that point,
plus any error or warning messages and positions
(so that, in unit tests, I can compare this generated output against reference output
for large numbers of manually and pseudo-randomly generated expressions).
Anyway, just having come back from VB.NET (where programming with interfaces is really second nature)
and forgetting that Delphi has basically botched the whole approach to interfaces,
I got stung, yet again, by the idiotically conflicting memory management between standard objects and interface based objects
(or, more precisely, by the absolutely stupid decision to default to reference counted memory management for all interface based objects).
Here, TNode is the ancestor class for parse tree nodes.
And an extract of the original code that implements the visitor pattern
to iterate over all nodes of the expression tree to build the resulting parse tree string:
INodeVisitor = interface;
TNode = class
procedure Accept(AVisitor: INodeVisitor); virtual; abstract;
end;
TNodeValueString = class(TNode)
FValue: String;
procedure Accept(AVisitor: INodeVisitor); override;
end;
INodeVisitor = interface
procedure Visit(ANode: TNodeValueString); overload;
. . .
end;
TToStringVisitor = class(TInterfacedObject, INodeVisitor)
FText: String;
procedure Visit(ANode: TNodeValueNumeric); overload;
. . .
function ToString(ARoot: TNode): string;
end;
A pretty standard set up of the pattern, I should think.
With the obvious implementations (I dropped the indenting part as it's not really relevant here):
{ TNodeValueString }
procedure TNodeValueString.Accept(AVisitor: INodeVisitor);
begin
AVisitor.Visit(self);
end;
{ TToStringVisitor }
procedure TToStringVisitor.Visit(ANode: TNodeValueString);
begin
FText := FText + '"' + ANode.FValue + '"';
end;
function TToStringVisitor.ToString(ARoot: TNode): string;
begin
FText := '';
ARoot.Accept(self);
Result := FText;
end;
To my surprise (and please remember, for the last three years I've been having a lot of fun programming with normally behaving interfaces)
FText was correct up to the last instruction:
Result := FText;
But before that assignment, FText was, all of a sudden, being reset to an empty string...
WTF!?!
Fortunately, I do remember some of the problems I had with using interfaces in Delphi.
Here, it turned out that the absolutely idiotic implementation of interfaces sets self's reference count to 0 at the start of the method,
increments it on the call to ARoot.Accept(self) and decrements it right after the call.
So, then, happily decides to destroy the object (self) before reaching the end of the method
(actually, it shouldn't destroy the object even then)...
And this isn't even a problem between mixing object and interface references - the well known issue in Delphi...
Multiple solutions are possible here, of course, but first, you have to understand the problem.
But why would you want to waste time on such stupidity when there are much more interesting things to do?
For instance, this looks very intuitive, no?
function TToStringVisitor.ToString(ARoot: TNode): string;
var
LSelf: INodeVisitor;
begin
FText := '';
LSelf := self;
ARoot.Accept(LSelf);
Result := FText;
end;
Nice, eh?
Though I am kind of joking here - another option is to turn off reference counting for the TToStringVisitor class entirely:
TToStringVisitor = class(TInterfacedObject, INodeVisitor)
. . .
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
. . .
function TToStringVisitor._AddRef: Integer;
begin
Result := -1;
end;
function TToStringVisitor._Release: Integer;
begin
Result := -1;
end;
But then, why default to reference counted memory management in the first place?
Why not just have interfaces as just contracts that have nothing to do with object deallocations?
Why should I manually turn off reference counting just because I'm calling a method and passing self as the parameter?
Man, at every single satisfaction survey I had asked Borladero to add non-reference counted interfaces to Delphi.
This is the one most important reason why I'm not so keen on coming back to the language (even considering recent developments)...
Anyway, enough of this rant...
Hope others don't stumble too often on similar problems - enough time wasted already.
Top
|