Dalija Prasnikar - Delphi Memory Managment
Dalija Prasnikar - Delphi Memory Managment
Dalija Prasnikar - Delphi Memory Managment
Page
Dalija Prasnikar
First Edition
Introduction
Memory management. One of the most basic parts of software
development, often kept on the side even though it has the most
profound effect on how we write our code.
Each memory management system has its good and bad sides. Each of
them offers solutions to some problems, but creates a whole range of
other problems, and each of them requires slightly different coding
patterns and practices. Understanding how memory management
systems work and knowing their strengths and weaknesses goes hand-
in-hand with writing clean, bug-free and maintainable code.
Both compilers will be covered in detail, along with the coding patterns
required for writing cross-compiler code that can run under both.
Acknowledgements
Huge thanks to the following engineers for providing the technical
reviews: William Meyer, David Erbas-White and Joe C. Hecht.
And last but not least, huge thanks to Allen Bauer and Barry Kelly for
sharing their vast knowledge of the implementation details of the Delphi
compilers. Without that, this book would not be possible.
Code
The primary purpose of the code examples provided in this book is to
explain certain concepts, show possible mistakes and bugs, and provide
basic templates for implementing coding patterns related to memory
management.
Spring4D - https://bitbucket.org/sglienke/spring4d
Clean Code
Code is read more times than it is written. Any kind of code where intent
is not clear and obvious will be inherently harder to read and therefore
more costly to maintain.
You may say, as long as it works it doesn't matter what kind of code it is.
But there is a world of difference between working code and good code.
Working code is when you have to squeeze a square peg into a round
hole and you manage to patch something together. It can certainly take
you home, but it is only a temporary solution nonetheless. You would not
want to rely on such a patch for long. What you actually want to have, is
a solution where a square peg goes into a square hole or a round peg
goes into a round hole. Anything else will only take you so far, if even.
https://en.wikipedia.org/wiki/KISS_principle
https://en.wikipedia.org/wiki/You_aren't_gonna_need_it
https://en.wikipedia.org/wiki/Don't_repeat_yourself
https://en.wikipedia.org/wiki/GRASP_(object-oriented_design)
Some say that SOLID is in contradiction with YAGNI and KISS. On the
contrary, they complement each other. YAGNI is about not adding
features until you really need them and not how to implement those
features. KISS is about how to keep designs simple and not over
engineering features and SOLID and GRASP are about how to write well
organized extendable and testable code, so when there is a need for
additional features they can be added with ease without breaking a whole
a lot of code.
General coding guidelines
Avoid clever code and use common coding patterns - clear code is preferred
Document unusual code and reasons behind it, where a seemingly
innocuous change might break it
Avoid fragile code - code that can easily be broken by subsequent changes
or changes in unrelated code
Avoid long methods, procedures, functions
Avoid coding patterns from other languages that don't fit well
Choose names carefully
Be consistent
Avoid clever code and use common coding patterns - clear code is
preferred
Nobody wants clever code in their code-bases. They want code that is
easy to understand and maintain. Code that can be read and understood
by many, not just by a single person.
Leave your smart code for T-shirts and social gatherings, and use clear
code in real life. Your future self will thank you.
The major difference and what really makes some code fragile, is not that
it can be broken by changes in surrounding code - any code can, for that
matter - but that it can be broken by changes in some seemingly
unrelated code far, far away.
Documenting such fragile code is of very little help. Tests can help
discover a sudden failure, but the best course of action would be avoiding
such coding patterns as much as possible.
One of the common examples in Delphi is the with construct, a relic from
the procedural programming era, that in object-oriented programming
does more harm than good. A new property, method, or a simple rename
refactoring can easily bring name collisions and different interpretation of
the code by the compiler.
Avoid writing long blocks of code. Code is read in blocks - the longer the
block, the more places there are for bugs to hide. Short, simple code is
more readable and easily understandable than long monolithic code.
When possible, long blocks (methods) should be broken down into
smaller logical parts.
Avoid coding patterns from other languages that don't fit well
For instance, if we have some visual control that can be shown or hidden,
we might define that functionality with two public methods Show and Hide
and a boolean field Visible that represents the current state.
type
TControl = class(TObject)
public
Visible: boolean;
procedure Show;
procedure Hide;
end;
procedure TControl.Show;
begin
Visible := true;
end;
procedure TControl.Hide;
begin
Visible := false;
end;
And then we can show the control either by calling the Show method or by
updating the Visible field.
var
Control: TControl;
...
Control.Show;
...
Control.Visible := true;
The problem with our Visible field is not the fact that it is publicly directly
accessible, but the fact we might want to introduce some changes in the
future. For instance, we might want to add some additional logic in our
Show method. But such a change would break any consumer code that
uses the Visible field directly.
procedure TControl.Show;
begin
Visible := true;
// some additional code that must be executed when control is shown
end;
procedure TControl.Show;
begin
Visible := true;
end;
We were able to change the class' behavior while maintaining the same
public API, without breaking consumer code. We can still change the
control's visibility by either using the Visible property or calling the Show
method.
Since Java does not have properties, the only way to change behavior is
removing direct access to the Visible field. That would break any
consumer code that uses Visible directly.
If you have ever had the pleasure of reading code that has been run
through some obfuscator, you know what I am talking about.
Similarly, badly named code would run just the same as properly named
code. While the compiler does not care about how good your names are,
humans do. And for better or for worse, they are still responsible for
writing, reading and understanding the code. Writing code requires more
than sticking your nose in a rose. You have to know it is a rose
beforehand, or you might get a nasty surprise.
Choose your names carefully, and make sure they clearly represent the
intent of the code behind them.
Be consistent
However, if you get to the point where consistency would hurt changes
and improvements, then improve the code by all means.
Don't wait too long. The best time for writing documentation is writing it
along with the code, while it is still fresh in your head. You will be able to
write it much faster and more accurately.
Any trouble you will have if you don't invest some time learning how to
use the code in question, will be solely self-inflicted. Even if you do spend
time learning, there will be things you might have missed and there may
be situations where not everything will work as expected, but way, way
less frequently than if you don't bother with reading at all.
If you think you don't have time to learn, think again. You will lose way
more time debugging later on. If you still think you don't have time, I can
only say, just RTFM.
The most distinguishing feature of ARC is the fact that just like manual
memory management, ARC is deterministic in its nature. That means
memory deallocation happens at a predictable time and place during
execution. While the reference counting mechanism adds some
overhead compared to manual memory management, due to thread
locking while incrementing and decrementing the reference count, it is
still consistent and repeatable, and just as with manual memory
management, any bottlenecks can be easily identified and, if necessary,
optimized.
Each object instance also uses more memory under ARC, because of the
accompanying reference count variable.
Garbage collection uses a stop the world model, where the process of
collecting object instances stops all other tasks until it is complete, and
can take long enough to impose visible slowdowns during application
execution. It is a runtime memory management model. It runs and
analyses objects' graphs at runtime, deciding which instances are ready
to be released. On the other hand, ARC reference counting code is
inserted during compile time at particular places in the code and only the
incrementing and decrementing of the reference count happens at
runtime.
While GC does not suffer from invalid reference issues and memory
leaks - caused by forgetting to release the object instance, or by
reference cycles - GC applications can also experience memory
problems, but in a different way. Memory leaks under GC are created by
holding onto references to an object that are no longer needed by the
application. Holding references to such unnecessary instances can have
devastating effects on application performance and memory
consumption, and can eventually lead to Out of Memory exceptions. As
long as there is an active reference to some object instance, GC will not
reclaim it, and such references can easily hide inside some long-lived
root nodes. Since those leaks are usually not obvious, tracking them
down can be extremely difficult. Just as with any other memory
management system, GC also requires a certain amount of discipline
when writing code.
Anyone saying that you don't have to know or think about how memory
management works under any kind of automatic memory management
system is lying to you. The thing is that with automatic systems, any
issues you might have will bite you much later in the process. Much later,
which usually means fixing bugs and issues might cost you more.
Historically, there were and still are, systems with different byte sizes, as
well as systems where the byte (regardless of its size) was not the
smallest addressable memory unit. However, for the purposes of this
book, and in the context of Delphi memory management, we will assume
that a byte contains 8 bits and the smallest addressable size is one byte.
The only thing that can vary is pointer (address) size, and that can be
either 32 or 64 bits, depending on the compiler used.
x := 42;
We have decided, for some arbitrary reason, that we need to have a byte
labeled as x . Whether this is a good name for that particular value is
anyone's guess, but the computer doesn't care if it is called x , HALF-OF-A-2D-
VECTOR , This_marks_the_Spot or thisIsAnExample . On the other hand, other
programmers - including yourself a few years after you have written a
certain piece of code - may object to your seemingly random naming
scheme.
var
x: integer;
x := 1024;
Oops. We may have retroactively deleted the Earth. (Since you are still
firmly on the ground, it's more likely that we simply edited the previous
image to represent an integer instead of a byte. This sort of thing doesn't
normally happen anyway.)
Variables are, well, variable and we can change the values stored inside
them. To prevent the destruction of the Earth - if it is not already too late -
we can update our integer variable x and safely store "42" again... until
next time...
var
x: integer;
x := 1024;
...
x := 42;
The second assignment will change the value stored inside the four bytes
variable x occupies, but its address will not change - it is still located at
$AB580F23.
Simple, right?
x := TObject.Create;
So far we have dealt with bytes and integers, which are fairly lightweight
and can be duplicated and thrown around with relative impunity. These
are often called value types, because their value is stored directly within
the space occupied by the variable. As a direct result of that fact,
assigning one value type variable to another will copy the stored value.
Once copied, changing the value of one variable will not change the
value of the other.
var
x, y: integer;
x := 42;
y := x;
x := 5;
Another important feature of value types is that their size is fixed and
cannot be changed at runtime. For the other kind of data, whose size can
vary and is often larger than the common data stored within value types,
we use reference types. When we are working with reference types, the
actual data is stored somewhere far away and the immediate value
stored in a variable of a reference type is a memory address called a
pointer or reference to the location where the actual data resides. Since a
pointer is an address to a memory location, its size is fixed, and
depending on the compiler it is either 32 or 64 bits.
Back to the x variant declared as TObject . TObject is a class, and classes are
reference types.
The difference is, this time we are not storing the contents of our TObject
instance in the four bytes assigned to x . Instead, we are storing where we
can find that instance: address $5FAC6041, in a memory chip far far
away. (From a certain point of view.) Any number of TObject references
could be pointing at that same address, possibly even changing its
contents, but the only one we are aware of right now is x .
2.2 Initialization and instantiation
One thing we have been glossing over so far is that variables -
regardless of whether they're value types or reference types - need to be
initialized. While some variables (notably, global variables or the various
fields in an object instance) will default to 0 or some equivalent value,
most variable types just grab whatever is currently occupying that
particular chunk of memory and run with it. Some reference types are
always automatically initialized with nil pointers - these are called
managed types and will be explained more thoroughly later - but others
are not.
Now, it should be pretty obvious how our first two examples initialized
their variables. We just set a value, and that was it. Sure, we deleted
Earth along the way, but that's why we have Undo.
But how did we initialize our TObject example? What does TObject.Create do?
Is it magic, is it sufficiently advanced technology, or is it just not known to
us? Do we even need to know?
Then came along the wise divine entity wizard programmer, who saw x 's
plight and took pity upon it. With a single assignment to TObject.Create , he
simultaneously created a new TObject and said, "Here is your TObject , x , far
away at $5FAC6041. Go now, and point the way towards it."
From that day, and until the programmer reassigned or deallocated it, x
would happily point to address $5FAC6041, in the distant land known
only as The Heap.
... Or we could skip the fairy tale and just say that TObject.Create
instantiates a new TObject instance on the heap and returns its address to
whatever called it so it can be stored - assigned to appropriate variable.
The latter scenario is called a stack overflow, and usually means that
something has gone horribly wrong. Actually, you have called one
function too many. On the bright side, the user will immediately know
something is wrong, because the application will crash. Then again, the
user won't be too happy while he is reporting the issue, so maybe that is
not so good.
If the function that throws the stack overflow is not recursive by design,
then it calls itself by mistake and is simply not designed to get out of that
loop.
We know that the stack only handles pointers and value types. We also
know that each stack is only accessible from a single thread. So, the
obvious question is, "What about the rest? Where do the pointers lead,
where is all the other data?"
The heap is the answer to that. While the stack always knows when,
where and how much memory it is going to allocate at any given time, the
heap has no such luxury. It doesn't know or care about where the data is
coming from or how big something is until the program tries to store it.
The heap merely keeps track of the free and occupied pieces of memory
in its realm.
Just like you can run out of stack memory, you can also run out of heap
memory and get the dreaded out of memory exception. Every time you
issue an allocation request to the heap, all the memory you requested at
that point must be allocated in one continuous chunk. If the heap cannot
find a free memory block of adequate size, it will deny your request and
an out of memory exception will be raised.
If your application has been a busy bee, allocating and deallocating a lot
of memory, especially in smaller different sized chunks, heap memory
can suffer from fragmentation. That means there is seemingly enough
free memory to approve a particular allocation request, but that free
memory is scattered around in small chunks that are not big enough to
satisfy that request.
Not all high-level OOP languages have raw pointer types. However,
Delphi does, and even though reference types are a special kind of
pointers, it is important to make a clear distinction between the two. The
similarities are only superficial, and they are fundamentally different from
a memory management point of view.
While all value types are actually managed by the compiler, the
terminology managed is specifically used for automatically managed
reference types.
There is one note about pointer types - a pointer on its own is actually a
value type. What makes them special is that for some pointer types, we
can allocate and deallocate memory manually. Those are the pointer
types we refer to when using the terms unmanaged or allocatable pointer.
x := 7;
p := @x;
var
p: ^Integer;
New(p);
p^ := 3;
Dispose(p);
Typed pointers can point only to a specific type and untyped ones are
general-purpose pointers that can point to any memory location.
As long as we are just reading such a shared string, there will be only
one copy. When we decide to change the value of the string, copy-on-
write will kick in. The contents of the string will be copied before it is
modified, and the code that tries to change it will actually get its own copy
to write to.
Delphi uses copy-on-write only for strings, however copy-on-write is a
widely used memory management concept that can be applied to other
types and situations.
There are three ways to inspect the contents of any pointer or reference:
One of the issues with invalid pointers is that during application execution
we cannot distinguish an invalid pointer from a valid one. There are some
debugging techniques and memory managers that can help us identify
such pointers, but they should be taken more as fire extinguishers. What
we really want is to prevent writing code that can cause fire in the first
place.
Checking any kind of invalid pointer for nil - either directly or using the
Assigned function - will give us a wrong result because both checks only
inspect the contents of the pointer or reference variable itself and not the
validity of what it references.
The following examples (for classic compilers) use the fact that
unmanaged local variables - including object references - are not
initialized by default. Since accessing invalid pointers is undefined, the
behavior and output of those examples may differ when compiled by
different compilers and when run on different computers. I tried to
compose all examples to exhibit erroneous (undesirable) output. If you
run those examples and get different results, take that as additional proof
for undefined behavior.
procedure Proc;
var
Obj: TObject;
begin
Writeln('Object address ', NativeUInt(Obj), ', is nil ',
Obj = nil, ', assigned ', Assigned(Obj));
Writeln(Obj.ToString);
end;
begin
Proc;
end.
procedure Clear;
var
x: NativeUInt;
begin
x := 0;
end;
procedure Proc;
var
Obj: TObject;
begin
Writeln('Object address ', NativeUInt(Obj), ', is nil ',
Obj = nil, ', assigned ', Assigned(Obj));
if Assigned(Obj) then Writeln(Obj.ToString)
else Writeln('Obj is nil');
end;
begin
Clear;
Proc;
end.
procedure Proc;
var
Obj: TObject;
Owner, Child: TComponent;
begin
// initialize Obj to nil for testing purposes
Obj := nil;
Writeln('Obj initialized');
Writeln('Obj address ', NativeUInt(Obj), ', is nil ',
Obj = nil, ', assigned ', Assigned(Obj));
Obj := TObject.Create;
Writeln('Obj created');
Writeln('Obj address ', NativeUInt(Obj), ', is nil ',
Obj = nil, ', assigned ', Assigned(Obj));
Writeln(Obj.ToString);
Obj.Free;
begin
Proc;
end.
Obj initialized
Obj address 0, is nil TRUE, assigned FALSE
Obj created
Obj address 15797696, is nil FALSE, assigned TRUE
TObject
Obj released
Obj address 15797696, is nil FALSE, assigned TRUE
TObject
Obj address 15797696, is nil FALSE, assigned TRUE
TMoveArrayManager<System.Classes.TComponent>
The first two value outputs are as expected - after initialization, Obj is nil ,
and after creation Obj is no longer nil and points to the memory address
(15797696) where the object instance is stored. Calling the ToString
method will show the class name TObject . It is important to note that with
each program execution, the actual memory address value can change,
but unless we assign another object instance or nil to the Obj variable, it
will keep the same value during a single execution.
The problems start when we release the object instance Obj refers to.
Afterthe object instance is released, its memory is no longer valid and Obj
has been freed, yet all value outputs are just the same as they were
when Obj pointed to a valid object instance. That is exactly issue with
dangling object references. At runtime we cannot tell the difference
between valid and dangling object references. We have to look at the
code and have to spot the logical error - using an object instance that we
have already released.
uses
System.Classes;
procedure Proc;
var
Obj, Ref: TObject;
Owner, Child: TComponent;
begin
Obj := TObject.Create;
Ref := Obj;
Writeln('Obj created, Ref assigned');
Writeln('Obj address ', NativeUInt(Obj), ', is nil ',
Obj = nil, ', assigned ', Assigned(Obj));
Writeln('Ref address ', NativeUInt(Ref), ', is nil ',
Ref = nil, ', assigned ', Assigned(Ref));
Writeln(Ref.ToString);
Obj.Free;
Obj := nil;
begin
Proc;
end.
The above example shows a similar scenario and outputs as the dangling
reference example, with a few differences. First, we now have two object
references, Obj and Ref , pointing to the same object instance - a perfectly
valid scenario. And second, after we released the object instance, we
also set its original reference Obj to nil. However, that didn't get us far. We
have reinitialized one reference, but we didn't reinitialize the other.
Regardless of what we do with the Obj variable, the variable Ref will still
point to an invalid memory location representing a stale reference. Stale
references may be a bit harder to spot in code than dangling ones,
because we have more than one identifier in play. In other words, it is
easier to see the error in the Obj.Free; ... Obj.DoSomething; sequence than in
the Obj.Free; ... Ref.DoSomething; sequence.
During program execution, entering scope is the point where the variable
becomes accessible by any part of the code. It is said that a variable
leaves its scope when code execution reaches the point from where the
variable is no longer accessible by any part of the code.
The lifetime of the variable is tightly coupled with its scope. Its lifetime will
begin just before it enters scope and will end just after it exits scope. Any
automatic initialization or finalization process will happen at that point
where the variable lifetime begins or ends.
However, the lifetime of the data behind the variables, while dependent
on variable scope, is not absolutely defined by it. When a variable
declared as a value type leaves its scope, the data associated with it is
gone. When a reference type variable leaves its scope, only the
reference itself is gone. The data behind the reference may have a
different lifetime from that of the reference itself, depending on whether or
not there are other references to that data, or whether we are dealing
with a managed or unmanaged reference type.
To make life less boring, the term scope carries some ambiguity. It can be
connected with the accessibility or the lifetime of the variable.
Having said that, the often used phrase in the context of clean and well
designed code about limiting scope covers both aspects. It actually
advises limiting the lifetime of the variable as much as possible and then
limiting the visibility as much as possible.
And now, let's shed some light on the whole scope, accessibility, visibility
and the lifetime mess.
The following are some commonly used phrases. Their definition should
not be taken too literally because they are often used in different contexts
where the exact meaning can slightly vary.
Local scope
It is safe to say that you should expect any implicit local variable's scope
to match the routine scope.
Global scope
Variables declared in the interface section of the units have global scope.
They are accessible from the very beginning of the program or module
that uses a particular unit to its end, and their lifetime starts with the
initialization of the unit and ends with its finalization.
Unit scope
Instance scope
Variables declared within a class are called fields. They are directly
accessible from within the class, depending on the declared visibility
specifiers, and indirectly through instance references if they have public
or published visibility. Their lifetime matches the instance's lifetime.
Class scope
Class fields' lifetime begins before the initialization section of the unit
where a class is declared and ends after the finalization section of that
unit. Their accessibility depends on their visibility specifiers. Class fields
are basically global variables in disguise.
Record scope
Delphi does not allow two identifiers with the same name declared in the
same block. Since every variable must be accessible within its defined
scope, there is a possibility of name conflicts when the scope of different
variables overlaps.
var
x: Integer;
procedure Proc;
var
x: Integer;
begin
x := 5;
Writeln('Local ', x);
// we are accessing x from outer scope with fully qualified name
Writeln('Global accessed from Proc ', Scope1.x);
end;
begin
x := 3;
writeln('Global ', x);
Proc;
Writeln('Global ', x);
Readln;
end.
There are two variables named x . One has global scope, the other has
local scope - for procedure Proc . When there are two variable names
accessible in the same block of code, the compiler will prefer the one with
the more localized scope. That is why assigning a value to x inside the
procedure will change the value of the local variable and not the global
one.
procedure Proc;
var
x: Integer;
begin
x := 5;
Writeln('Local ', x);
end;
begin
x := 3;
writeln('Global ', x);
Proc;
Writeln('Global ', x);
Readln;
end.
var
x: Integer;
procedure Proc;
begin
x := 5;
Writeln('Local ', x);
end;
begin
x := 3;
writeln('Global ', x);
Proc;
Writeln('Global ', x);
Readln;
end.
A bit more insight into records and classes from Joe C. Hecht:
Classes and objects are really no more than ordinary records containing
function tables, and a pointer to self, used to neatly encapsulate data
(now called properties) with the functions (now called methods) that
manipulate the data. A pointer to self is passed to each function to allow
the methods to find the record with which they are associated.
In fact, if you want to roll up your sleeves, you can extend any language
that does not directly support object oriented programming, providing the
language supports function variables (most often often called function
pointers) to allow the language to support OOP by simply adding a
function table and a pointer to self to a record! In fact, if you explore the
history of C++, you will find that its first existence was constructed on C
with macros.
Inheritance is formed when new records descend from other records, and
concepts such as virtual functions are put into place by copying,
manipulating, and extending the function tables in the record.
Believe it or not, there are cases where you might still need to roll up your
sleeves and do this sort of thing, for example, extending a language such
as "C" that does not directly support Object Oriented Programming to
support an object oriented framework that you design yourself.
4.1 Fields
Variables declared within classes (and records, for that matter) are called
fields. They are just ordinary variables that can be accessed only through
class instances - objects.
Classes can also define fields that belong to the class itself using the
class prefix.
4.2 Properties
Properties define an interface for accessing specific data in the class.
They can be directly mapped to fields, or can be accessed using
methods called property getters and setters - as a result, they don't have
to actually exist at any particular place in memory.
Since they only represent an access interface, they don't have any
associated memory management requirements or rules.
4.3 Methods
Methods are not all that dissimilar to ordinary functions and procedures.
They take parameters, they do things with them, and they may or may
not return a value after they are done.
On the other hand, there are some notable differences, and a lot of ways
to further modify their behavior.
reintroduce;
overload;
virtual | dynamic | override;
register | pascal | cdecl | stdcall | safecall;
abstract;
platform; deprecated; library;
Besides following the ordering and exclusivity rules, there are no other
strict requirements for method directives, and methods can have zero or
more combinations of the listed directives.
4.3.2 Self
Since methods are declared as part of a class, and a single class can
have many instantiated objects, we need a binding mechanism between
methods and a particular class instance. This is done with Self , a special
identifier that is accessible from within methods and refers to the
particular object instance upon which the method has been called upon.
TShip = class(TObject)
private
// Some kind of array or collection of TPart instances.
// The details don't matter right now.
public
destructor Destroy; override;
procedure AddPart(APart: TPart);
end;
TPart = class(TObject)
public
constructor Create(AShip: TShip);
end;
implementation
destructor TShip.Destroy;
begin
// Make sure to do whatever destruction is necessary on our part storage clas
inherited;
end;
Delphi does not require that all access to an object's fields are prefixed
with Self . It is commonly only used when we need to reference the object
itself.
4.3.3 inherited
is used to invoke the methods of a parent class. If the
inherited
parameters in the parent class match the parameters of the method from
which inherited is called, then you can simply call inherited without
specifying anything else. Otherwise, the method must be called as usual,
but prefixed with the inherited keyword.
Here's one of the more common examples of the former kind of inherited
call - constructors and destructors:
type
TQuestion = class(TObject)
public
FAnswer: string;
constructor Create;
destructor Destroy; override;
end;
implementation
constructor TQuestion.Create;
begin
// Calls the parent class' constructor
inherited;
destructor TQuestion.Destroy;
begin
// Do whatever special destruction our class requires.
// In this case, we don't actually need to define a destructor,
// because there is nothing to destroy.
And here's an example of the other kind of inherited call, located in the
TChild.DifferentPrint method:
type
TParent = class
public
// The virtual keyword is necessary for method overriding.
procedure Print; virtual;
end;
TChild = class(TParent)
public
procedure Print; override;
procedure DifferentPrint;
end;
procedure TParent.Print;
begin
WriteLn('This is the parent''s output.');
end;
procedure TChild.Print;
begin
// We are not calling inherited here because we want to print our own stuff.
WriteLn('This is the child''s output.');
end;
procedure TChild.DifferentPrint;
begin
// Calls TChild's immediate parent's Print function.
// In this case, that's TParent.Print.
inherited Print;
end;
var
c: TChild;
begin
c := TChild.Create;
c.Print;
end.
TSquare = class(TRectangle)
public
procedure Draw;
end;
procedure TRectangle.Draw;
begin
Writeln('Draw rectangle');
end;
procedure TSquare.Draw;
begin
Writeln('Draw square');
end;
var
Rectangle: TRectangle;
Square: TSquare;
begin
Rectangle := TRectangle.Create;
// Calls TRectangle.Draw
Rectangle.Draw;
Rectangle.Free;
Rectangle := TSquare.Create;
// Still calls TRectangle.Draw,
// even though the TSquare subclass has a Draw function of its own
Rectangle.Draw;
// Casts Rectangle to a TSquare and calls TSquare.Draw on it.
// This would crash the program if Rectangle weren't a TSquare.
TSquare(Rectangle).Draw;
Rectangle.Free;
Square := TSquare.Create;
// Calls TSquare.Draw
Square.Draw;
Square.Free;
end.
The trouble is, we don't necessarily know when we're dealing with a
TSquare instead of a TRectangle . This is where dynamic binding comes in.
Delphi provides two keywords for dynamic binding: virtual and dynamic .
Both of them achieve the same effect - the difference is in how they do it.
In short, virtual methods are optimized to work as quickly as possible,
whereas dynamic methods generate smaller code, but work more slowly.
It is all very well to know the keywords and the theory of how dynamic
binding works, but none of that is any good if you don't know how to
apply it. How does one use dynamic binding in Delphi?
implementation
procedure TSomeObject.DoSomething;
begin
WriteLn('I identify with the number 42.');
end;
The thing is, this does not really do much on its own. We don't have any
subclasses overriding DoSomething - we don't have any subclasses at all,
really - and there are numerous things we could do beyond simply
overriding it.
The override directive tells the compiler that we want to replace calls to our
parent's method with calls to our method, regardless of what we are
being labeled as.
implementation
procedure TAnotherObject.DoSomething;
begin
WriteLn('I prefer the number 47.');
end;
AnOverrideTest := TAnotherObject.Create;
AnOverrideTest.DoSomething;
AnOverrideTest.Free;
end.
Right?
type
TAnotherObject = class(TSomeObject)
public
procedure DoSomething; override; final;
end;
TYetAnotherObject = class(TAnotherObject)
public
procedure DoSomething; override;
end;
Then let's try compiling again. The program will fail to compile,
generating this error:
E2352 Cannot override a final method
There are three things we can do to solve the issue we've created:
Now let's try redeclaring TSomeObject.DoSomething like this, and see what the
compiler does:
procedure DoSomething; virtual; abstract;
The reintroduce directive, inserted before any other directives, stops the
compiler from throwing warnings about hidden methods. It also tells other
programmers, including your future self, that you meant to hide a certain
method, and that it should stay this way.
We now know how to override a method. We've also learned how to hide
a method. We already knew how to do nothing.
TType2 = class(TType1)
public
procedure OverriddenMethod; override;
procedure HiddenMethod; reintroduce; virtual;
procedure TestMethod;
end;
procedure TType1.OverriddenMethod;
begin
WriteLn('TType1 Override');
end;
procedure TType1.HiddenMethod;
begin
WriteLn('TType1 Hidden');
end;
procedure TType1.IgnoredMethod;
begin
WriteLn('Calling Override ...');
OverriddenMethod;
end;
procedure TType2.OverriddenMethod;
begin
WriteLn('TType2 Override');
end;
procedure TType2.HiddenMethod;
begin
WriteLn('TType2 Hidden');
end;
procedure TType2.TestMethod;
begin
WriteLn('Internal inheritance test ...');
inherited OverriddenMethod;
OverriddenMethod;
inherited HiddenMethod;
HiddenMethod;
inherited IgnoredMethod;
IgnoredMethod;
end;
var
Object1: TType1;
Object2: TType2;
begin
Object1 := TType1.Create;
Object1.OverriddenMethod;
Object1.HiddenMethod;
Object1.IgnoredMethod;
Object1.Free;
Object1 := TType2.Create;
Object1.OverriddenMethod;
Object1.HiddenMethod;
Object1.IgnoredMethod;
Object1.Free;
Object2 := TType2.Create;
Object2.OverriddenMethod;
Object2.HiddenMethod;
Object2.IgnoredMethod;
Object2.TestMethod;
Object2.Free;
end.
TType1 Override
TType1 Hidden
Calling Override...
TType1 Override
TType2 Override
TType1 Hidden
Calling Override...
TType2 Override
TType2 Override
TType2 Hidden
Calling Override...
TType2 Override
Finally, we take a look at how all of these various methods are seen from
the perspective of TType2 . When we make inherited calls to the methods, we
always get TType1 's implementation, but when we make a regular call, we
get the same implementation we would get if we had called the methods
directly on Object2 without the TestMethod wrapper: TType2.OverriddenMethod ,
TType2.HiddenMethod , and TType1.IgnoredMethod .
But what if those classes did not know their sizes? What if we had to
pass them in our method calls? That's simple enough, anyways:
type
TRectangle = class(TObject)
public
procedure Draw(AWidth, AHeight: integer);
end;
// For now, let's assume squares draw the same way as rectangles.
TSquare = class(TRectangle);
TSquare = class(TRectangle)
public
// This is why we made Draw virtual - to point out that
// overloading a virtual method will prompt the compiler
// to throw W1010 warnings at us again unless we reintroduce it.
// Overloading a static method doesn't
// require the 'reintroduce' keyword, though.
procedure Draw(ASize: integer); reintroduce; overload;
end;
Another way of dealing with the above situation is giving up on our TSquare
class completely, and adding both Draw methods to the TRectangle class.
Depending on which one we call, we would draw either a square or a
rectangle.
type
TRectangle = class
public
procedure Draw(AWidth, AHeight: integer); overload;
procedure Draw(ASize: integer); overload;
end;
var
Rectangle: TRectangle;
begin
Rectangle := TRectangle.Create;
Rectangle.Draw(10, 5);
Rectangle.Draw(10);
end.
There are two ways we can call our DoSomethingClassy method above, both of
which will do the same thing. The only difference is in whether we are
calling it from an object instance or from the class itself:
var
Obj: TClassy;
begin
Obj := TClassy.Create;
Obj.DoSomethingClassy;
// Let's short-circuit the counter a bit, shall we?
TClassy.ClassLevel := 4;
TClassy.DoSomethingClassy;
Having any kind of memory leaks, no matter how small they are, is
always a bad thing. The point is that occasionally leaking a few bytes of
memory might go unnoticed for a long time, while the issues caused by
draining system resources are way more prominent.
For instance, if an application creates or opens a file handle in exclusive
mode (usually to write something), and fails to release that handle when
it is done, it will not only block itself from doing anything else with that
particular file, but it will also prevent all other applications or the operating
system from using it.
destructor TFileStream.Destroy;
begin
if FHandle <> INVALID_HANDLE_VALUE then
FileClose(FHandle);
inherited Destroy;
end;
The file resource is acquired during construction, and if that fails, so will
the constructor. The release of the acquired file resource will happen
inside the object instance's destructor, and not sooner.
var
fs: TFileStream;
begin
fs := TFileStream.Create('C:\foo.txt', fmCreate or fmShareExclusive);
try
fs.Write();
finally
fs.Free;
end;
end;
In pure ARC code that does not have to work cross-platform, you may be
tempted to cut it down in the following manner:
var
fs: TFileStream;
begin
fs := TFileStream.Create('C:\foo.txt', fmCreate or fmShareExclusive);
fs.Write();
Even though that code seems fine, it will fail for a rather simple reason.
The execution of the code during the assignment is not happening from
left to right, but from right to left - the constructor runs first and then the
newly constructed object is assigned to the reference, resulting in the
destruction of the previous object instance. In the above code, that
implies the construction of the new file stream while the existing one still
holds the lock, resulting in the second constructor's failure.
Even if we use separate variables for writing and reading, we would still
have to explicitly release the first file stream before we can create the
second one. And this is the kind of code where we have to think about
implicit references too. Such hidden strong references holding our first
file stream would interfere with that stream's destruction and break our
code. Assigning nil to the fs variable would not destroy the file stream at
that point as we desired:
var
fs: TFileStream;
begin
fs := TFileStream.Create('C:\foo.txt', fmCreate or fmShareExclusive);
fs.Write();
fs := nil;
One could say that null issues are more related to the lack of proper
language support for dealing with null than with null itself. While this may
seem like an oversight in language designs that failed to implement such
support, there is another problem - adding any type of checks and
specialized handling increases the complexity of compilers and the
resulting code - at the time that would be considered more of a luxury,
than a necessity.
When you look at it, all the variety of languages we have and their
continuous evolution comes from the desire to figure out which are the
tasks at which humans are inherently bad, and what can be done to
move such tasks to the less fallible machine, thus making humans more
productive and more focused on the real work.
Nil represents a valid pointer or reference state. Nil is merely the absence
of a value, not a problem on its own. Our problems arise from the way we
deal with nil values. Just as zero plays an important role in math and you
can still crash your application if you divide by zero. The unexpected
usage of a nil reference represents a clear bug in code that needs to be
fixed just like any other bug we can encounter using or abusing any other
language feature.
Nil is the default value of a pointer or reference before you assign some
valid value to it - like assigning a newly created object instance. If you did
not have nil as a reliable flag that tells you whether your reference points
to a valid object or not, you would have to deal with something much
worse - wild pointers.
You might think that the solution to the above would be creating a special
object that does nothing. So accessing nil would not crash your
application. Having this kind of nothing substitute for nil would just delay
our problem - accidental usage of a nil reference - because if you expect
nil and you forget to test for it, it is a bug in the program. If you don't
expect nil and you get it, it is a bug in the program.
"Do nothing" behavior would just replace those bugs with other ones. If
you expect a nothing reference and you forget to test for it, it is bug in the
program. If you don't expect nothing and you get it, it is a bug in the
program. Replacing those bugs that result in application crash, with an
equivalent bug that does not crash does not give you a properly working
application.
Some languages take dealing with nil to another level and provide
additional compile-time safety. Delphi does not have such capabilities,
but knowing about how and what kind of problems they solve can help in
better understanding why nil is or isn't so bad.
One of the problems with nil is that sometimes you want some references
to never, ever be nil. Ensuring that in Delphi is the developer's job - if you
fail, your application will crash at runtime. Some languages provide a
means of marking variables as non-nil, also called non-optional. Using
them moves responsibility from the developer to the compiler and
subsequently replaces having runtime errors with compile-time errors,
significantly reducing the potential for bugs.
If you think the above can cover every case and you are home free,
never having to deal with unexpected nil references, think again.
Unchecked nil references are still possible even there. Completely
eradicating nil from a language is hard, and not always a viable option,
especially in general purpose languages and those that can get close to
the metal. But with proper and better language support it can be less of a
problem.
While Delphi does not have any nil safety capabilities built in, it is not so
bad either. Unlike some popular languages, managed (default) string
types in Delphi cannot hold nil and that greatly reduces potential nil
issues.
There are also some rules and patterns that can help in avoiding nil
reference problems, like using the null object pattern; protecting access
to lazy objects; limiting the scope of reference types; and creating object
instances at the very beginning of their scope and releasing them at the
end, making their lifetime as close as possible to the lifetime of the
reference they are stored in.
Nil is the cause of a certain range of problems, to be sure. But not all
languages suffer equally from it, and even in those languages that do
suffer more it is still quite an exaggerated problem. And that brings
another set of problems to the table.
Don't do that. Delphi has nil as a value and not much compiler safety to
go with it. No amount of dancing around it will give you safety that only
changes to the language and compiler can bring. It will only make your
code more convoluted. Any time you deal with nil and your code starts to
look like a KISS violation, you are most likely doing it wrong.
Don't sprinkle your code with unnecessary nil checks. Check for nil only
those references where nil is an expected value by design, not those
where nil can occur by mistake. Use exception handling at appropriate
points to deal with mistakes and unpredictable errors.
Don't combine object references with any kind of boolean or other kind of
flags where a plain nil flag will suffice.
Don't abuse the lazy object pattern. If you are dealing with an object
instance that has to be available the whole time its reference is in scope,
then just create it at the first opportunity - when it enters the scope. Lazy
is not a safety belt replacement for trivial mistakes like forgetting to
construct the object at a certain point. Using lazy where it is not
appropriate obscures the code's intent.
Don't abuse the null object pattern. It is not a good fit for every scenario.
Use null object only when your code logic does not care whether it uses a
null object or a valid object instance. If you are merely replacing nil value
checks with checking whether your object is a null object, then you are
certainly misusing null object. Additionally, the lack of automatic memory
management in Delphi makes null object even less appropriate
compared to other languages that do have automatic memory
management.
program NilException;
uses
System.SysUtils,
System.Classes;
type
TFoo = class(TObject)
public
First: integer;
Second: string;
end;
var
Foo: TFoo;
begin
Foo := nil;
try
Foo.Destroy;
except on E: Exception do
Writeln(E.Message);
end;
try
Writeln(Foo.First);
except on E: Exception do
Writeln(E.Message);
end;
try
Writeln(Foo.Second);
except on E: Exception do
Writeln(E.Message);
end;
end.
Remember, for larger classes that have many fields, close to zero does
not necessarily have to be really close. For example, the size of a TForm
object instance is larger than 900 bytes.
If the address you are trying to read is really large, something more like
408dc337 - that is an indication that you are accessing an invalid non-nil
pointer.
Nevertheless, the ability to recognize and track down simple issues fast
can be quite a time saver.
Delphi has two ways of testing for nil values. One is comparing reference
to nil with equality (or inequality) operators or using the intrinsic function
System.Assigned
Assigned(P) corresponds to the test P <> nil for a pointer variable, and
@P <> nil for a procedural variable.
type
TProcedure = procedure;
procedure Print;
begin
Writeln('Printing');
end;
var
p: TProcedure;
begin
p := Print;
if p <> nil then p;
end.
Replacing p with @p can be compiled and the comparison would work
properly, regardless of the value in p .
if @p <> nil then p;
So, what is the big deal? Typing @ is faster than typing Assigned and it is
also quite readable. And the compiler will not allow you to compile
syntactically incorrect code.
Well, not so fast... compiler protection is not something you can count on.
type
TObjectFunction = function: TObject;
var
f: TObjectFunction;
begin
f := GetObject;
if f <> nil then Writeln('f is not nil')
else Writeln('f is nil');
end.
Can you guess what will happen if you try running the above code? No, it
will not give you a compile-time error. It will compile perfectly. But the
output will be somewhat unexpected.
f is nil
How can f be nil when you assigned the GetObject function to it just one
line before???
Since there is no @ before the procedural variable f , the compiler will not
compare the contents of the variable with nil , it will call the function
stored in f and compare its result to nil instead. GetObject returns nil
yielding False in the if expression.
What is worse, if the content of the f variable is nil , the whole thing will
blow up with an access violation exception. Only because you forgot to
write one single @ .
And when you get back to the code, it is easy to miss the fact you are
missing @ . Life is too short to be wasted on such trivialities. Assigned is the
way to go!
If you are worried about the performance of Assigned , stop worrying. Assigned
is an intrinsic compiler function which means the compiler can replace
that function call with specialized, highly optimized code and does not
have to call any function at all. In fact, the generated code for @p <> nil
and Assigned(p) will be exactly the same, something along these lines:
if @p <> nil then p;
if Assigned(p) then p;
Since Delphi does not have a default way of marking whether nil is an
acceptable value or not, before using any new library, framework or any
other code you are not familiar with, you have to read the documentation
first and find out what kind of values you can expect.
Using nil is fast, simple and flexible. If we look at all code used in some
application through a pyramid chart where the base consists of core
frameworks and reusable multipurpose code, and the peak is for highly
specialized application-specific code, then the code in the base of the
pyramid will be the code where nil will be used more, where using it
better caters to the different needs of the code built on top of it, and code
near the top is the code where nil can be replaced with other patterns,
where appropriate.
One of the most prominent use cases for passing nil is the TComponent
constructor. TComponent can maintain a hierarchy of owned objects. Each
component has an Owner property that points to its owner, another
TComponent . How do you define an owner for the object sitting at the top of
the hierarchy? Simply by passing nil to the constructor, indicating the
object has no owner. This could also be accomplished by having an
additional parameterless constructor. However, maintaining a single
virtual constructor simplifies the construction of different TComponent
descendants via metaclasses, as used in Delphi's component streaming
system.
Another highly used pattern for passing nil is using setter methods for
some optional collaborator objects, where assigning nil to a property is
redirected to a setter behind the scenes. Similarly to setter methods,
getter methods for such properties can also use nil as a valid value and
return it.
As far as regular functions returning nil are concerned, one of the use
cases is locating specific objects in some collection. If an object
conforming to the required parameters is not found, nil can be returned.
As has already been said, used improperly the null object pattern just
shifts potential problems to another level. If we have a crashing
application because we mistreated nil value, then the mere introduction
of null object will not magically solve our code logic. It will merely replace
crashing with some other form of buggy behavior that can be even harder
to find than the bugs manifested by an ordinary crash. However, that
does not mean null object is useless everywhere, it merely points out that
null object is not a universal solution.
Null object is useful in places where you don't care whether you deal with
a null object or a valid object instance. For example, when you have
some optional collaborator objects, that have to perform some task,
replaceable with do nothing behavior.
The TLogger class is an abstract class that defines a logging API. TNullLogger
is declared as a separate class to give us a clear idea how to implement
null object and do nothing behavior, and TConsoleLogger is a logger
implementation that actually does something.
Instead of having an abstract base TLogger class we could start with a non-
abstract TLogger class that would implement do nothing behavior by default
and avoid separate TNullLogger , but separating the base API (interface)
declaration from different implementations, including the null one, is
clearer in intent and purpose of each and every class.
program NullObjectPattern;
uses
System.SysUtils,
System.Classes;
type
TLogger = class(TObject)
public
procedure Log(const Msg: string); virtual; abstract;
end;
TNullLogger = class(TLogger)
public
procedure Log(const Msg: string); override;
end;
TConsoleLogger = class(TLogger)
public
procedure Log(const Msg: string); override;
end;
TRocket = class(TObject)
protected
Logger: TLogger;
public
constructor Create(ALogger: TLogger);
procedure Design;
procedure Build;
procedure Launch;
procedure Go;
end;
procedure TRocket.Design;
begin
Logger.Log('Designing');
// designing code goes here
end;
procedure TRocket.Build;
begin
Logger.Log('Building');
// rocket building code goes here
end;
procedure TRocket.Launch;
begin
Logger.Log('Launching');
// whatever you need to launch the rocket
Logger.Log('Looking good');
end;
procedure TRocket.Go;
begin
Logger.Log('Going to the Moon');
Design;
Build;
Launch;
Logger.Log('See you there...');
end;
var
SomeLogger: TLogger;
Rocket: TRocket;
begin
SomeLogger := TConsoleLogger.Create;
try
Rocket := TRocket.Create(SomeLogger);
try
Rocket.Go;
finally
Rocket.Free;
end;
finally
SomeLogger.Free;
end;
end.
We can change the behavior from the outside, without changing TRocket 's
inner workings. Without the null object pattern we could achieve the
same, allowing Logger to be nil and then do nil checks every time we want
to log something. However, in this case using a null object makes more
sense. It prevents access violation exceptions if we forget a nil check
before using Logger , and at the same time, it makes the code more
readable without any negative side effects.
For instance, a Launch method that can operate on a nil Logger would have
to be updated in the following manner:
procedure TRocket.Launch;
begin
if Assigned(Logger) then Logger.Log('Launching');
// whatever you need to launch the rocket
if Assigned(Logger) then Logger.Log('Looking good');
end;
One of the issues with the null object pattern is that you have to provide a
specialized null object class that will implement do nothing behavior, and
often a single null object instance. In our logger example, maintaining a
single null logger instance is not necessary. Regardless of which logger
class you use, there will be either one or very few instances required. On
the other hand, using the null object pattern in some tree-like structure
with many leaf nodes, maintaining a single null node instance that can
serve as an endpoint for all leaves is imperative.
This makes the null object pattern more appropriate for automatic
management systems, since you don't have to manually release objects.
Under manual memory management, you have to be careful not to
release your null object instance, and protecting that instance from
accidental release can be as hard as protecting accidental access to nil .
The first implementation uses plain nil as leaf objects and will serve as a
base for building null object implementations. All implementations will
count the number of instantiated nodes, for later comparison.
program TreeNil;
var
Count: Integer;
type
TNode = class(TObject)
strict private
FValue: Integer;
FLeft, FRight: TNode;
public
constructor Create(AValue: Integer);
destructor Destroy; override;
property Value: Integer read FValue;
property Left: TNode read FLeft write FLeft;
property Right: TNode read FRight write FRight;
end;
TBinarySearchTree = class(TObject)
protected
function DoSearch(AValue: Integer; ANode: TNode): TNode;
procedure DoPrint(ANode: TNode);
public
Root: TNode;
destructor Destroy; override;
procedure Insert(AValue: Integer);
function Search(AValue: Integer): TNode;
procedure Print;
end;
destructor TNode.Destroy;
begin
FLeft.Free;
FRight.Free;
inherited;
end;
destructor TBinarySearchTree.Destroy;
begin
Root.Free;
inherited;
end;
procedure TBinarySearchTree.Print;
begin
DoPrint(Root);
end;
var
Tree: TBinarySearchTree;
begin
ReportMemoryLeaksOnShutdown := true;
Tree := TBinarySearchTree.Create;
try
Tree.Insert(6);
Tree.Insert(5);
Tree.Insert(3);
Tree.Insert(2);
Tree.Insert(9);
Tree.Insert(4);
Tree.Insert(7);
Tree.Insert(8);
Tree.Insert(1);
Tree.Print;
Running our base BST implementation will yield the following output:
1
2
3
4
5
6
7
8
9
10 not found
Total count: 9
As we can see, our BST works perfectly, properly sorted. We can also
test whether some value is stored in the BST or not. All other
implementations must provide the same output. Then the only thing that
can vary is the Total count representing the number of node object
instances we have created. For a nil -based BST that is exactly the
number of values we added to the tree. There is no overhead when it
comes to creating nodes, because our leaf nodes are plain nil flags.
Our first null object BST implementation will construct a separate null
node for each endpoint.
program TreeNullObject;
var
Count: Integer;
type
TNode = class(TObject)
strict protected
FValue: Integer;
FLeft, FRight: TNode;
function GetLeft: TNode; virtual;
function GetRight: TNode; virtual;
procedure SetLeft(const Value: TNode);
procedure SetRight(const Value: TNode);
public
constructor Create(AValue: Integer);
destructor Destroy; override;
function IsNull: boolean; virtual;
property Value: Integer read FValue;
property Left: TNode read GetLeft write SetLeft;
property Right: TNode read GetRight write SetRight;
end;
TNullNode = class(TNode)
strict protected
function GetLeft: TNode; override;
function GetRight: TNode; override;
public
constructor Create;
function IsNull: boolean; override;
end;
TBinarySearchTree = class(TObject)
protected
function DoSearch(AValue: Integer; ANode: TNode): TNode;
procedure DoPrint(ANode: TNode);
public
Root: TNode;
constructor Create;
destructor Destroy; override;
procedure Insert(AValue: Integer);
function Search(AValue: Integer): TNode;
procedure Print;
end;
destructor TNode.Destroy;
begin
FLeft.Free;
FRight.Free;
inherited;
end;
constructor TNullNode.Create;
begin
Inc(Count);
end;
destructor TBinarySearchTree.Destroy;
begin
Root.Free;
inherited;
end;
if Root.IsNull then
begin
Root.Free;
Root := Node;
end
else
begin
Current := Root;
while true do
begin
Parent := Current;
if AValue < Parent.Value then
begin
Current := Current.Left;
if Current.IsNull then
begin
Parent.Left := Node;
break;
end;
end
else
begin
Current := Current.Right;
if Current.IsNull then
begin
Parent.Right := Node;
break;
end;
end;
end;
end;
end;
function TBinarySearchTree.DoSearch(AValue: Integer; ANode: TNode): TNode;
begin
if ANode.IsNull or (AValue = ANode.Value) then Result := ANode
else
if AValue < ANode.Value then Result := DoSearch(AValue, ANode.Left)
else Result := DoSearch(AValue, ANode.Right);
end;
procedure TBinarySearchTree.Print;
begin
DoPrint(Root);
end;
var
Tree: TBinarySearchTree;
begin
ReportMemoryLeaksOnShutdown := true;
Tree := TBinarySearchTree.Create;
try
Tree.Insert(6);
Tree.Insert(5);
Tree.Insert(3);
Tree.Insert(2);
Tree.Insert(9);
Tree.Insert(4);
Tree.Insert(7);
Tree.Insert(8);
Tree.Insert(1);
Tree.Print;
And our second null object implementation of a Binary Search Tree will
use a common, single null node instance.
program TreeNullObjectSingle;
var
Count: Integer;
type
TNode = class(TObject)
strict protected
FValue: Integer;
FLeft, FRight: TNode;
function GetLeft: TNode; virtual;
function GetRight: TNode; virtual;
procedure SetLeft(const Value: TNode);
procedure SetRight(const Value: TNode);
public
constructor Create(AValue: Integer);
destructor Destroy; override;
function IsNull: boolean; virtual;
property Value: Integer read FValue;
property Left: TNode read GetLeft write SetLeft;
property Right: TNode read GetRight write SetRight;
end;
TNullNode = class(TNode)
public
class var Instance: TNode;
class constructor ClassCreate;
class destructor ClassDestroy;
strict protected
function GetLeft: TNode; override;
function GetRight: TNode; override;
public
constructor Create;
function IsNull: boolean; override;
end;
TBinarySearchTree = class(TObject)
protected
function DoSearch(AValue: Integer; ANode: TNode): TNode;
procedure DoPrint(ANode: TNode);
public
Root: TNode;
constructor Create;
destructor Destroy; override;
procedure Insert(AValue: Integer);
function Search(AValue: Integer): TNode;
procedure Print;
end;
destructor TNode.Destroy;
begin
if not Left.IsNull then FLeft.Free;
if not Right.IsNull then FRight.Free;
inherited;
end;
constructor TNullNode.Create;
begin
Inc(Count);
end;
constructor TBinarySearchTree.Create;
begin
Root := TNullNode.Instance;
end;
destructor TBinarySearchTree.Destroy;
begin
if not Root.IsNull then Root.Free;
inherited;
end;
procedure TBinarySearchTree.Print;
begin
DoPrint(Root);
end;
var
Tree: TBinarySearchTree;
// code using single null object implementation
// is the same as code using multiple one
...
Let's take a look at the code and compare the differences. But before we
start looking at the code, we should look at the memory overhead.
For n nodes added to the tree, the total number of instantiated objects is:
The real comparison here is between the nil and single null object BST
implementations.
Besides having the additional TNullNode class, our base TNode class
declaration is more complex. In order to implement the do nothing
behavior in the descendant TNullNode class, instead of plain field access to
the Left and Right nodes, we had to implement virtual getters and setters.
Also, we need the IsNull function, which will tell us which kind of node we
are dealing with.
Our constructor has more work to do. Besides assigning the stored value,
we also have to assign a shared null node instance. Destruction of the
null nodes also brings another level of complexity.
Now, let's see if all that additional work is justified. Did we get any real
safety? Have we simplified the code that uses null nodes?
Absolutely not.
In all the code that deals with possible null nodes we have replaced nil
checks with IsNull checks. If we forget the IsNull check in the DoPrint
method we are trading an access violation exception for a stack overflow
exception. Overall, our internal BST code is no less error-prone or
simpler. On the contrary, it is more convoluted and brings only a false
sense of security, as it feels like we have strengthened our code when we
actually made no improvement whatsoever.
Further profiling shows that the null object implementation is also about
twice as slow as the original one.
While interfaces and their functionality are covered later on, it makes
sense to cover nullable types under the Nil chapter. It is very difficult to
present these issues in a linear fashion, and you may need to jump
around a bit, depending on your prior experience.
You cannot change the base concept of a hard-coded interface, and the
rest of the code is simple enough. While there may be slight variations,
most implementations look rather similar at their core.
One thing that sticks out is the previously mentioned FHasValue field that is
declared as an interface and not a plain boolean flag. This is solely due
to the fact that Delphi does not have automatic initialization of records.
Without default automatic initialization, our boolean flag might start its life
with any random value - it can be False and it can be True . To avoid error-
prone manual initialization of a nullable record, we are exploiting the fact
that Delphi always initializes managed fields. Using an interface instead
of a boolean will give us automatic initialization and our nullable record
will always start its life properly initialized - HasValue will always be false,
until we actually store some value inside.
More about the fake interface mechanism and how it actually works can
be found in the Interfaces chapter.
unit Nullable;
interface
uses
System.SysUtils,
System.Classes,
System.Generics.Defaults;
type
TNullable<T> = record
strict private
FValue: T;
FHasValue: IInterface;
function GetValue: T;
function GetHasValue: boolean;
public
constructor Create(AValue: T);
function ValueOrDefault: T; overload;
function ValueOrDefault(Default: T): T; overload;
property HasValue: boolean read GetHasValue;
property Value: T read GetValue;
implementation
const
FakeInterfaceVTable: array [0 .. 2] of Pointer =
(@NopQueryInterface, @NopAddRef, @NopRelease);
FakeInterfaceInstance: Pointer = @FakeInterfaceVTable;
function TNullable<T>.GetValue: T;
begin
if not HasValue then
raise Exception.Create('Invalid operation, Nullable type has no value');
Result := FValue;
end;
function TNullable<T>.ValueOrDefault: T;
begin
if HasValue then Result := fValue
else Result := default(T);
end;
end.
One of the advantages of value types is they cannot hold nil . They
always have a value, not always a meaningful one, but it seems that
people would rather deal with bugs caused by random values than nil
exceptions. The belief that nil is a horrible, horrible invention is so
widespread that finding a proper undisputable use case for Nullable is not
an easy task.
procedure TWeatherReport.Print;
begin
Write('Hour: ', Hour);
Write(', Temp: ', Temperature);
Writeln(', Wind: ', Wind);
end;
begin
Report.Hour := 10;
Report.Temperature := 20;
Report.Wind := 5;
Report.Print;
end.
Great, everything works just fine. Why on Earth would we need any
Nullable here?
Hour: 10, Temp: 20, Wind: 5
Since we are bounded by integers, there is no other way than to use -274
degrees Celsius as an impossible value. And we happily fill our report
and print it.
Report.Hour := 11;
Report.Temperature := -274;
Report.Wind := 4;
Report.Print;
And before you know it, you start getting calls from people thinking we
are looking at a new ice age.
Turns out, -274 was not the greatest value after all. But let's not blame
the value. We can adjust our code. If the Temperature is -274 we would not
print it. We can do same with the Wind field - if the value is -1, we didn't
measure it.
procedure TWeatherReport.Print;
begin
Write('Hour: ', Hour);
if Temperature = -274 then Write(', Temp: not measured')
else Write(', Temp: ', Temperature);
if Wind = -1 then Writeln(', Wind: not measured')
else Writeln(', Wind: ', Wind);
end;
Report.Print;
During lunch hour, our coworker decides to put -300. For good measure.
And there goes the ice age again.
Report.Hour := 13;
Report.Temperature := -300;
Report.Wind := 4;
Report.Print;
Until the night shift comes in. Absolute zero? Piece of cake -269.
Hour: 23, Temp: -269, Wind:6
By now, you are probably convinced that having some arbitrary value as
a flag is a rather error-prone solution.
This is where Nullable comes in. If we declare the Temperature and Wind
fields as TNullable<Integer> , and adapt our Print procedure we have solved
the problem. If we cannot write an actual measured value, we just don't
use any. Nil clearly describes no value, undefined value intent, both for
developers writing and reading the code as well as for people using the
application.
type
TWeatherReport = record
public
Hour: Integer;
Temperature: TNullable<Integer>;
Wind: TNullable<Integer>;
procedure Print;
end;
procedure TWeatherReport.Print;
begin
Write('Hour: ', Hour);
if Temperature.HasValue then Write(', Temp: ', Temperature.Value)
else Write(', Temp: not measured');
if Wind.HasValue then Writeln(', Wind: ', Wind.Value)
else Writeln(', Wind: not measured');
end;
Report.Hour := 23;
Report.Wind := 6;
Report.Print;
Ice age problem solved. Now if only we could get rid of global warming
that easily...
6 Ownership
There are two aspects of ownership. The first one is strictly about the
relationship between object instances, and the second is about a
responsibility to release an object instance when it is no longer needed.
Regardless of the aspect, references to object instances play a
significant part in ownership models as they define both aspects -
relationships and responsibilities.
6.0.2 Owner
The ownership relation between different objects is expressed through
owning references contained in an object that points to other object
instances. An object that contains an owning reference to another object
is called an owner.
Every object instance must have one and only one owning reference
at any time. Following that rule is of the utmost importance under manual
memory management. This is the best way we can avoid premature
releases or double releases of our object instance.
1. construction - creation
2. active lifetime
3. destruction
When the compiler runs into code that calls a constructor like
TSomething.Create , or a destructor such as SomeObject.Destroy , it will not simply
call those methods like it would do with any other method, but will insert a
rather elaborate code sequence behind the scenes.
The following diagram depicts the whole lifecycle and all the important
steps that happen during the construction and deconstruction stages.
Delphi is an exception-safe language. This means that properly handled
exceptions during construction can and will leave the application in a fully
operational state.
7.1 Difference in object instance lifecycle between
classic and ARC compiler
The basic workflow for object instance construction and destruction is the
same in classic and ARC compilers. There are some differences in the
actual code executed due to the reference counting mechanism in the
ARC compiler, but all object instances will always go through the same
four construction and destruction stages.
Under the classic compiler, all construction stages will execute as one
sequential block; all destruction stages will also execute as a block. The
ARC compiler has the same behavior for object construction, but its
object destruction behavior depends on whether or not DisposeOf has been
called on the object instance. If destruction is triggered without involving
DisposeOf , it will execute as one block just like it would under the classic
compiler. If DisposeOf is called, stages 1 and 2 will be executed immediately
as one block, and stages 3 and 4 as another that will be executed at later
time - introducing some side-effects.
The implicit first parameter is Self , just like with any other method, but Self
can represent a reference to either a class or an object instance. If a
constructor is called through a class or metaclass, Self will contain a
reference to that class, and the value of DoAlloc will be set to True indicating
that we are dealing with a class and that we want to create a new object
instance. That will result in calling the NewInstance class method which will
actually construct and return it. On the other hand, when a constructor is
called on an object instance, DoAlloc will be False , indicating that we are
already dealing with an object instance passed through the Self
parameter.
procedure Hello;
var
sl: TStringList;
begin
sl := TStringList.Create;
sl.Add('Hello World');
end;
And this is just about all we have to do under the ARC compiler, since the
destruction of object instances is automatically managed by the compiler.
Under manual memory management, the above code would create a
memory leak since we have to take care of destroying our TStringList
instance when we are done using it. Destruction is triggered by calling
the Free method on the object reference we want to destroy.
...
sl.Free;
end;
"No problem," you may say. Calling Free on a nil reference is a safe thing
to do, so you are just making a redundant call and nothing else.
Wrong!
While functionally this kind of code would work perfectly, it is not exactly a
text-book example of code you should be using. When it comes to
wrapping temporary object creation in try...finally blocks, the previous
HelloFree example should be used as proper template. But, there is a
reason why I am showing HelloFreeInit here, beyond it merely being an
example of bad code.
Now, that escalated quickly. Even if you separated the creation logic from
the actual work to be done, and moved that part into another procedure,
you would still have quite a mess on your hands.
If we apply the logic from the HelloFreeInit example, we can actually merge
multiple try...finally blocks into one, significantly improving code
readability.
procedure GreatParty;
var
Foo: TFoo;
Bar: TBar;
FooBar: TFooBar;
begin
Bar := nil;
FooBar := nil;
Foo := TFoo.Create;
try
Bar := TBar.Create;
FooBar := TFooBar.Create;
// do some real Foo Bar
finally
Foo.Free;
Bar.Free;
FooBar.Free;
end;
end;
procedure Free;
procedure DisposeOf; inline;
7.4.3 Constructors
7.4.4 Destructors
7.4.6 AfterConstruction
7.4.7 BeforeDestruction
Constructors follow the same overriding rules as all methods, but the fact
that the TObject class has a default public constructor Create will somewhat
limit your ability to achieve certain things.
Let's start with constructors and the fact that TObject has a default static
parameterless constructor. Declaring any public constructor, with or
without parameters, will hide that default parameterless constructor. That
prevents you from calling the wrong constructor if your class needs to be
constructed with certain parameters. If you don't declare any new
constructors inside the class, its ancestors' constructors will be
automatically available. What you cannot do is completely hide all
constructors.
type
TFoo = class(TObject)
strict private
constructor Create;
public
end;
constructor TFoo.Create;
begin
inherited;
Writeln('TFoo constructor');
end;
var
Foo: TFoo;
begin
Foo := TFoo.Create;
Foo.Free;
end.
Running the above example will output nothing, because the constructor
declared in TFoo is hidden and cannot be accessed outside the class. The
default constructor declared in the TObject class is accessible and it will be
called instead. If the TFoo constructor is moved from the strict private
section to the public section, you will get TFoo constructor as output. There is
no way you can prevent instantiation of an object instance in publicly
available classes by hiding its constructors.
When declaring multiple constructors with the same name and different
parameters, you will have to use the overload directive. Overloading
constructors will also include overloaded constructors from the ancestors.
However, if you don't use the overload directive in the descendant class,
you will hide ancestor constructors.
Consider the following code: TFoo has two overloaded constructors, but
TBar does not and it will hide them. Trying to call TBar.Create(1) and invoking
a TFoo constructor overload will fail to compile with an error:
type
TFoo = class(TObject)
public
constructor Create; overload;
constructor Create(AValue: integer); overload;
end;
TBar = class(TFoo)
public
constructor Create;
end;
constructor TFoo.Create;
begin
inherited;
Writeln('TFoo');
end;
constructor TBar.Create;
begin
inherited;
Writeln('TBar');
end;
var
Bar: TBar;
begin
Bar := TBar.Create;
Bar.Free;
end.
TFoo
TBar
Adding the overload directive to the TBar constructor will remove the
compiler error, and we will be able to construct TBar by directly invoking
the TFoo constructor. However, if we directly invoke the TFoo constructor,
the TBar constructor call will be skipped, and if the TBar constructor
contained any critical code, we would need to add the appropriate
constructor overload to the TBar class.
TBar = class(TFoo)
public
constructor Create; overload;
end;
...
Bar := TBar.Create(1);
TFoo Integer
destructor TFoo.Destroy;
begin
// your finalization code here
...
// call inherited Destroy
inherited;
end;
The following is the minimal and the most common initialization and
finalization pattern of an owned inner object instance:
type
TBar = class(TObject)
end;
TFoo = class(TObject)
private
FBar: TBar;
public
constructor Create;
destructor Destroy; override;
end;
constructor TFoo.Create;
begin
inherited;
FBar := TBar.Create;
end;
destructor TFoo.Destroy;
begin
FBar.Free;
inherited;
end;
Those methods are underutilized, almost to the point that makes you
wonder if developers are even aware of their existence and purpose.
Sometimes, we are just too lazy or keeping it simple. Overriding and
customizing additional methods is not always worth the trouble,
especially in short code, but in complex classes with long inheritance
chains, that is less of a concern and many issues related to initialization
or finalization order, especially in destructor chains, could easily be
solved by moving the appropriate code from constructors and destructors
into the AfterConstruction and BeforeDestruction methods. Any code that needs
checking whether the object or its parts may already have been
constructed or destroyed is a good candidate for moving. Of course, that
is not always possible due to other possible requirements or interactions
with existing code, but it is an option well worth exploring.
This is the most basic example of how to properly declare and customize
the above mentioned methods:
type
TFoo = class(TObject)
public
destructor Destroy; override;
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
end;
destructor TFoo.Destroy;
begin
Writeln('Destroy');
inherited;
end;
procedure TFoo.AfterConstruction;
begin
inherited;
Writeln('AfterConstruction');
end;
procedure TFoo.BeforeDestruction;
begin
inherited;
Writeln('BeforeDestruction');
end;
var
Foo: TFoo;
begin
Foo := TFoo.Create;
Foo.Free;
end.
The following example has two classes, TFoo and TFooBar , that inherit from
TFoo . All methods are properly declared and overridden, and all
implementations correctly call their inherited methods.
type
TFoo = class(TObject)
public
constructor Create;
destructor Destroy; override;
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
end;
TFooBar = class(TFoo)
public
constructor Create;
destructor Destroy; override;
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
procedure Run;
end;
constructor TFoo.Create;
begin
inherited;
Writeln('TFoo Create');
end;
destructor TFoo.Destroy;
begin
Writeln('TFoo Destroy');
inherited;
end;
procedure TFoo.AfterConstruction;
begin
inherited;
Writeln('TFoo AfterConstruction');
end;
procedure TFoo.BeforeDestruction;
begin
inherited;
Writeln('TFoo BeforeDestruction');
end;
constructor TFooBar.Create;
begin
inherited;
Writeln('TFooBar Create');
end;
destructor TFooBar.Destroy;
begin
Writeln('TFooBar Destroy');
inherited;
end;
procedure TFooBar.AfterConstruction;
begin
inherited;
Writeln('TFooBar AfterConstruction');
end;
procedure TFooBar.BeforeDestruction;
begin
inherited;
Writeln('TFooBar BeforeDestruction');
end;
procedure TFooBar.Run;
begin
Writeln('Running');
end;
var
FooBar: TFooBar;
begin
FooBar := TFooBar.Create;
try
FooBar.Run;
finally
FooBar.Free;
end;
end.
If we remove the call to inherited inside the TFooBar constructor, we can see
that the inherited constructor will never be executed. The same goes for
any other method. Without calling inherited , we break the inherited
execution chain.
constructor TFooBar.Create;
begin
Writeln('TFooBar Create');
end;
Results in:
TFooBar Create
TFoo AfterConstruction
...
Usually you can place a call to inherited at any point in the method call, as
long as the logic of your code does not depend on any particular code in
the inherited chain being executed or not. Since finalization logic is
usually the reverse of the initialization logic, the most common ordering is
calling inherited first in constructors, and last in destructors.
type
TFoo = class(TObject)
public
Lines: TStringList;
constructor Create;
destructor Destroy; override;
end;
TFooBar = class(TFoo)
public
Copy: TStringList;
constructor Create;
destructor Destroy; override;
procedure AfterConstruction; override;
end;
constructor TFoo.Create;
begin
inherited;
Lines := TStringList.Create;
Lines.Add('Line');
end;
destructor TFoo.Destroy;
begin
Lines.Free;
inherited;
end;
constructor TFooBar.Create;
begin
inherited;
Copy := TStringList.Create;
Copy.Text := Lines.Text;
end;
destructor TFooBar.Destroy;
begin
Copy.Free;
inherited;
end;
procedure TFooBar.AfterConstruction;
begin
inherited;
Writeln('Lines created: ', Assigned(Lines));
end;
var
FooBar: TFooBar;
begin
try
FooBar := TFooBar.Create;
try
Writeln(FooBar.Copy.Text);
finally
FooBar.Free;
end;
except
on E: Exception do Writeln(E.ClassName, ': ', E.Message);
end;
end.
Changing the position of the inherited call in the TFooBar constructor will
produce an access violation exception.
constructor TFooBar.Create;
begin
Copy := TStringList.Create;
Copy.Text := Lines.Text; // Lines instance is not constructed at this point
inherited;
end;
constructor TFooBar.Create;
begin
Copy := TStringList.Create;
Writeln('TFooBar constructor Lines created: ', Assigned(Lines));
inherited;
end;
The program will run without exceptions and will show that the Lines
instance is successfully created when we reached the AfterConstruction
method, but it is still not initialized inside the TFooBar constructor before we
called inherited .
On the other hand, changing the location of inherited in the destructor will
not have any negative impact on our test code, because in this case the
destructors don't have any dependencies that would require a different
order of execution.
This is also a good place to repeat that destructors should never throw
exceptions, and must be able to deal with half-constructed object
instances. So if the order of destructor execution matters, make sure that
you don't forget to protect against possible exceptions that might be
raised if the constructors didn't have the chance to complete their work.
Usually, the order of execution in any of the above methods should not
matter, except in the case of construction and destruction of dependent
objects. For instance, if some previously constructed object must be
passed as a parameter to the constructor of some other object field. The
destruction order of such connected instances may also be strictly
defined.
If the order of execution matters in other cases, that might indicate that
the order-dependent code might not belong in constructors or
destructors. For instance, the code in the TFooBar constructor that initializes
Copy from the Lines instance could be moved into AfterConstruction , and we
could safely place calls to inherited anywhere we like.
constructor TFooBar.Create;
begin
Copy := TStringList.Create;
inherited;
end;
procedure TFooBar.AfterConstruction;
begin
inherited;
Writeln('Lines created: ', Assigned(Lines));
Copy.Text := Lines.Text;
end;
There is no strict rule that requires moving such dependent code from
constructors and destructors to AfterConstruction and BeforeDestruction . In
simple code, like the previous example, it really does not matter where
the Copy.Text initialization is, as long as the code is properly written. It is up
to the developer to decide when moving particular code from constructors
or destructors is beneficial.
Also, if necessary, you can skip calls to immediate ancestor(s) and call
any particular ancestor constructor, prefixing the constructor call with the
ancestor's type. If you do that, make sure that you implement any vital
functionality from the constructors you have skipped.
type
TFoo = class(TObject)
public
constructor Create;
end;
TBar = class(TFoo)
public
constructor Create;
end;
TFooBar = class(TBar)
public
constructor Create;
end;
constructor TFoo.Create;
begin
inherited;
Writeln('TFoo constructor');
end;
constructor TBar.Create;
begin
inherited;
Writeln('TBar constructor');
end;
constructor TFooBar.Create;
begin
// skip call to TBar constructor and call TFoo constructor
TFoo.Create;
Writeln('TFooBar constructor');
end;
var
FooBar: TFooBar;
begin
FooBar := TFooBar.Create;
FooBar.Free;
end.
Class constructors and destructors are called automatically for any used -
touched - class and that implies automatic execution of all class
constructors and destructors in its ancestor chain - you don't have to call
inherited in your class constructor or destructor.
Since there is no way to tell which class will be used and which won't in
dynamically loaded packages via the LoadPackage function, all contained
classes' constructors and destructors will be executed.
There are two major differences between using class constructors and
destructors compared to the commonly-used initialization and finalization
sections.
Second, class destructors run after the unit finalization section, and at
that point all managed variables - including strings - will already be
cleared at that point so forget about doing anything substantial there,
besides merely releasing allocated class fields.
type
TBase = class
public
class var Value: Integer;
class var Text: string;
class constructor ClassCreate;
class destructor ClassDestroy;
end;
begin
Writeln(TBase.Value);
Writeln(TBase.Text);
end.
If you put a breakpoint into the above class destructor, you will see that
the integer field Value contains the expected value of 5, but the Text field
will be empty.
8.6 Object construction through metaclasses
Class reference types or metaclasses are types used for referencing
classes and declared as class of type where type is an actual class.
Besides the class used in its declaration, metaclass references can also
hold any of that class' descendant classes.
Just like TObject is a root type that can hold any object instance, TClass is a
root metaclass type that can hold any class.
Common examples are TCollection , in which stored items are created with
the help of the TCollectionItemClass metaclass, and the component streaming
system, in which components are created during the streaming process
using TComponentClass .
Let's take a look at the following classes. Note that none of the
constructors are virtual.
type
TFoo = class(TObject)
public
Text: string;
constructor Create;
end;
TBar = class(TFoo)
public
constructor Create;
end;
constructor TFoo.Create;
begin
inherited;
Text := Text + 'Foo';
end;
constructor TBar.Create;
begin
inherited;
Text := Text + 'Bar';
end;
Now let's construct some objects in different ways. In all of the examples
that follow, the class of the constructed object instance will be the correct,
expected one, regardless of whether we are hard-coding the class or
using a metaclass.
Obj := TFoo.Create;
Writeln(Obj.ClassName, ', ', TFoo(Obj).Text);
=> TFoo, Foo
If we use TFoo as a variable type instead of TObject , there will be no
changes in behavior. The only difference is that we don't have to typecast
Obj to TFoo in order to access the Text field.
var
Obj: TFoo;
Obj := TFoo.Create;
Writeln(Obj.ClassName, ', ', Obj.Text);
=> TFoo, Foo
Similarly, if we explicitly use the TBar class for object construction, our
object instance will be properly created, regardless of the assignment's
variable type.
Obj := TBar.Create;
Writeln(Obj.ClassName, ', ', Obj.Text);
=> TBar, FooBar
If you think that the following example will yield the same result as the
first one, you might get a nasty surprise. The created object instance will
be of the TFoo class, but the TFoo constructor will not be invoked???
var
MetaClass: TClass;
Obj: TObject;
MetaClass := TFoo;
Obj := MetaClass.Create;
Writeln(Obj.ClassName, ', ', TFoo(Obj).Text);
=> TFoo,
Still does not make any sense, does it? Why is the object constructed
with the right type, while the wrong constructor gets called?
MetaClass.Create
The above piece of code is crucial. How does that piece of code work?
FooClass := TFoo;
Obj := FooClass.Create;
FooClass := TBar;
Obj := FooClass.Create;
Creating a TBar instance with TFooClass should work now. Well, not really.
We have marked TFoo.Create as virtual, but TBar.Create is still a static
constructor. If we look at the compiler output we can notice the following
warning pointing to the Create declaration in TBar class:
TBar = class(TFoo)
public
constructor Create; override;
end;
var
FooClass: TFooClass;
Obj: TObject;
FooClass := TBar;
Obj := FooClass.Create;
TBar = class(TFoo)
public
constructor Create(const AName: string); override;
end;
TFooClass = class of TFoo;
var
FooClass: TFooClass;
Obj: TFoo;
FooClass := TBar;
Obj := FooClass.Create('Nothing');
var
Titles: TStrings;
begin
Titles := TStringList.Create;
try
MovieTitles(Titles);
// use Titles
....
finally
Titles.Free;
end;
end;
var
Titles: TStrings;
begin
Titles := MovieTitles;
try
// use Titles
...
finally
Titles.Free;
end;
end;
Object factories do have their place in Delphi, but they should be used
only when their primary purpose is to construct various objects, not when
a new object is merely a side effect. Naming such functions also plays a
significant role. A function that constructs the new object should have a
clear name that indicates its intent and purpose.
The Get... prefix in Delphi is commonly used for property getters and
should never be used for functions that transfer ownership.
Create..., New... , Make... , Build... are all appropriate, choosing the right one
also depends on the actual use case. Since Create is commonly used as a
constructor name, it should be avoided if there is the possibility of a
conflict or misinterpretation.
TFooBar = class(TFoo)
public
constructor Create; override;
end;
TBar = class(TObject)
public
constructor Create;
end;
constructor TFoo.Create;
begin
inherited;
Writeln('TFoo Create');
end;
constructor TFooBar.Create;
begin
inherited;
Writeln('TFooBar Create');
end;
constructor TBar.Create;
begin
inherited;
Writeln('TBar Create');
end;
Obj := ObjectFactory(TFooBar);
Obj.Free;
Obj := ObjectFactory(TObject);
Obj.Free;
end.
In this factory, we are using the InheritsFrom class function that determines
whether an object instance or - in our case, class - is or descends from
the class passed as its parameter. If we used the equality operator
instead, we would not be able to call the appropriate constructors for
descendant classes of TFoo and TBar .
function ObjectFactory(ObjClass: TClass): TObject;
begin
if ObjClass = TFoo then Result := TFooClass(ObjClass).Create
else
if ObjClass = TBar then Result := TBarClass(ObjClass).Create
else Result := ObjClass.Create;
end;
Obj := ObjectFactory(TFooBar);
type
{$RTTI EXPLICIT METHODS([vcProtected, vcPublic])}
TWork = class
protected
procedure Use(Obj: TFoo); overload;
procedure Use(Obj: TBar); overload;
procedure Use(Obj: TObject); overload;
public
procedure Run(Instance: TObject);
end;
repeat
for Method in Methods do
begin
Params := Method.GetParameters;
if (Length(Params) = 1) and (Method.Name = 'Use') then
begin
ParamType := Params[0].ParamType;
if (ParamType.IsInstance) and
(ParamType.AsInstance.MetaclassType = CurrentClass) then
begin
Method.Invoke(Self, [Instance]);
exit;
end;
end;
end;
CurrentClass := CurrentClass.ClassParent;
until CurrentClass = nil;
end;
var
Obj: TObject;
Work: TWork;
begin
Work := TWork.Create;
try
Obj := ObjectFactory(TBar);
try
Work.Run(Obj);
finally
Obj.Free;
end;
Obj := ObjectFactory(TFooBar);
try
Work.Run(Obj);
finally
Obj.Free;
end;
Obj := ObjectFactory(TObject);
try
Work.Run(Obj);
finally
Obj.Free;
end;
finally
Work.Free;
end;
end.
TBar Create
Using Bar TBar
TFoo Create
TFooBar Create
Using Foo TFooBar
Using Object TObject
One of the commonly used classes that does that is TCustomForm - it defines
two events, OnCreate and OnDestroy , that can be used from within the Form
Designer and which provide an additional place to construct and destroy
additional objects and perform additional initialization.
TCustomForm is a rather complex class, and the actual code behind OnCreate
and OnDestroy events, combined with the rest of the initialization and
finalization code, is not exactly straightforward. In order to present the
OnCreate and OnDestroy pattern as a simple template that can be extended
and applied for other purposes, I will cut the TCustomForm code down to the
point where it will no longer fully represent the exact initialization and
finalization behaviors of TCustomForm .
type
TCustomForm = class(TComponent)
private
FOnCreate: TNotifyEvent;
FOnDestroy: TNotifyEvent;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AfterConstruction; override;
procedure BeforeDestruction; override;
published
property OnCreate: TNotifyEvent read FOnCreate write FOnCreate;
property OnDestroy: TNotifyEvent read FOnDestroy write FOnDestroy;
end;
constructor TCustomForm.Create(AOwner: TComponent);
begin
inherited;
end;
destructor TCustomForm.Destroy;
begin
inherited;
end;
procedure TCustomForm.AfterConstruction;
begin
inherited;
if Assigned(FOnCreate) then FOnCreate(Self);
end;
procedure TCustomForm.BeforeDestruction;
begin
inherited;
if Assigned(FOnDestroy) then FOnDestroy(Self);
end;
The OnCreate and OnDestroy events are published properties of the TCustomForm
class, and can be assigned through the Form Designer and then properly
connected through the form streaming mechanism, or they can be
attached through code. Since their primary purpose in TCustomForm is to
enable RAD simplicity for customizing forms, attaching them through
code makes very little sense in the context of forms.
This pattern was presented not only to show the behavior of forms, but
also to provide a template for applying a similar pattern in other cases.
For example, instead of having OnCreate and OnDestroy declared as events
that can be hooked through Form Designer, you can use anonymous
methods instead, to inject custom behavior into the class without the
need to declare a descendant class at all. Of course, such patterns do
not have to be related only to instance lifecycle, and can be used in other
places and for other purposes.
If the Assign function does not recognize the Source object class and does
not know how to perform copying, you should call the default
TPersistent.Assign implementation, which will call the AssignTo method on the
Source in case the Source class supports copying for the destination object
class. If the AssignTo method does not know how to handle the incoming
object, it will raise an exception. Combined with AssignTo , Assign allows you
not only to create clones of the same type, but also to copy instances of
some other class.
var
Source, Dest: TSomePersistent;
...
We will start with the base TShape class and its descendants TCircle , TSquare
and TUnicorn .
The Assign methods of TSquare and TCircle both know how to create copies
of each other, and AssignTo will not be invoked if we try copying one to the
other.
To show how AssignTo works, and how it can be used to extend deep
copying to new classes that don't have to be known by the Source class,
nor need to have a common ancestor besides TPersistent , we will use
TFloatShape and its descendant TFloatCircle . To implement AssignTo , one of the
classes has to know about the other. In this case, TShape does not have
any knowledge of TFloatShape or its descendant classes, but TFloatShape does
know about TShape and therefore can implement an AssignTo method that will
allow copying between those two classes and their respective class
hierarchies.
The TNotShape class will be another demonstration of classes that are not
mutually assignable. You cannot assign a TShape instance to a TNotShape ,
instance nor you can assign TNotShape to a TShape instance. Doing so will
raise an EConvertError exception.
Note the calls to inherited methods. They are crucial for implementing the
particular behavior.
interface
uses
System.Classes,
System.Types;
type
TShape = class(TPersistent)
protected
FName: string;
FPosX: Integer;
FPosY: Integer;
public
procedure Assign(Source: TPersistent); override;
procedure Write; virtual;
property Name: string read FName write FName;
property PosX: Integer read FPosX write FPosX;
property PosY: Integer read FPosY write FPosY;
end;
TCircle = class(TShape)
protected
FDiameter: Integer;
public
procedure Assign(Source: TPersistent); override;
procedure Write; override;
property Diameter: Integer read FDiameter write FDiameter;
end;
TSquare = class(TShape)
protected
FSize: Integer;
public
procedure Assign(Source: TPersistent); override;
procedure Write; override;
property Size: Integer read FSize write FSize;
end;
TUnicorn = class(TShape)
protected
FMagicSize: Integer;
public
procedure Assign(Source: TPersistent); override;
procedure Write; override;
property MagicSize: Integer read FMagicSize write FMagicSize;
end;
implementation
type
PPersistent = class(TPersistent);
procedure TShape.Write;
begin
Writeln('Shape ', Name, ' X: ', PosX, ' Y: ', PosY);
end;
procedure TCircle.Write;
begin
Writeln('Circle ', Name, ' X: ', PosX, ' Y: ', PosY, ' D: ', Diameter);
end;
procedure TSquare.Assign(Source: TPersistent);
begin
if Source is TCircle then
begin
FSize := TCircle(Source).Diameter;
inherited;
end
else
if Source is TSquare then
begin
FSize := TSquare(Source).Size;
inherited;
end
else
if Source <> nil then PPersistent(Source).AssignTo(Self)
else inherited;
end;
procedure TSquare.Write;
begin
Writeln('Square ', Name, ' X: ', PosX, ' Y: ', PosY, ' Size: ', Size);
end;
procedure TUnicorn.Write;
begin
Writeln('Unicorn ', Name, ' X: ', PosX, ' Y: ', PosY,
' Magic Size: ', MagicSize);
end;
end.
unit FloatShapes;
interface
uses
System.Classes,
System.Types;
type
TFloatShape = class(TPersistent)
protected
FName: string;
FX: Double;
FY: Double;
procedure AssignTo(Dest: TPersistent); override;
public
procedure Assign(Source: TPersistent); override;
procedure Write; virtual;
property Name: string read FName write FName;
property X: Double read FX write FX;
property Y: Double read FY write FY;
end;
TFloatCircle = class(TFloatShape)
protected
FDiameter: Double;
procedure AssignTo(Dest: TPersistent); override;
public
procedure Assign(Source: TPersistent); override;
procedure Write; override;
property Diameter: Double read FDiameter write FDiameter;
end;
implementation
uses
Shapes;
procedure TFloatShape.Write;
begin
Writeln('Float Shape ', Name, ' X: ', X:0:2, ' Y: ', Y:0:2);
end;
procedure TFloatCircle.Write;
begin
Writeln('Float Circle ', Name, ' X: ', X:0:2, ' Y: ', Y:0:2,
' D: ', Diameter:0:2);
end;
end.
program DeepCopy;
{$APPTYPE CONSOLE}
uses
System.Sysutils,
System.Classes,
Shapes in 'Shapes.pas',
FloatShapes in 'FloatShapes.pas';
type
TNotShape = class(TPersistent)
protected
FName: string;
FText: string;
public
procedure Write;
procedure Assign(Source: TPersistent); override;
property name: string read FName write FName;
property Text: string read FText write FText;
end;
procedure TNotShape.Write;
begin
Writeln('I am not a shape ', name, ' ', Text);
end;
var
Circle, SecondCircle, ThirdCircle: TCircle;
Square: TSquare;
FloatCircle: TFloatCircle;
Unicorn, PinkUnicorn: TUnicorn;
NotShape: TNotShape;
begin
Circle := TCircle.Create;
SecondCircle := TCircle.Create;
ThirdCircle := TCircle.Create;
Square := TSquare.Create;
FloatCircle := TFloatCircle.Create;
Unicorn := TUnicorn.Create;
PinkUnicorn := TUnicorn.Create;
NotShape := TNotShape.Create;
Circle.Name := 'Blue';
Circle.PosX := 5;
Circle.PosY := 2;
Circle.Diameter := 10;
Circle.Write;
SecondCircle.Assign(Circle);
Circle.Free;
Circle := nil;
SecondCircle.Write;
Square.Assign(SecondCircle);
Square.Write;
FloatCircle.Assign(SecondCircle);
FloatCircle.Write;
ThirdCircle.Assign(FloatCircle);
ThirdCircle.Write;
try
Unicorn.Assign(Square);
except
on E: Exception do Writeln(E.ClassName, ': ', E.Message);
end;
try
Square.Assign(Unicorn);
except
on E: Exception do Writeln(E.ClassName, ': ', E.Message);
end;
Unicorn.Name := 'Pink';
Unicorn.PosX := 1;
Unicorn.MagicSize := 3;
Unicorn.Write;
PinkUnicorn.Assign(Unicorn);
PinkUnicorn.Write;
try
NotShape.Assign(Square);
except
on E: Exception do Writeln(E.ClassName, ': ', E.Message);
end;
try
Square.Assign(NotShape);
except
on E: Exception do Writeln(E.ClassName, ': ', E.Message);
end;
SecondCircle.Free;
ThirdCircle.Free;
Square.Free;
FloatCircle.Free;
NotShape.Free;
end.
The first two lines are the result of initializing a Circle instance and
assigning it to SecondCircle . Since both Circle and SecondCircle are instances
of the same class, they are completely identical. After we have made a
deep copy, we can release the instance referenced by Circle and we can
independently use SecondCircle .
Square Blue X: 5 Y: 2 Size: 10
The above line was printed after SecondCircle was assigned to Square . Those
instances belong to different types, so they are not clones. We merely
allowed copying data from one object instance to another based on their
class. Square is still a square, but now it represents a square that just
happens to have the same parameters as our SecondCircle .
Square.Assign(SecondCircle);
Square.Write;
Next, we copied SecondCircle to FloatCircle . They are both circles, but use
different coordinate systems. To prove that copying works in both
directions, we assigned FloatCircle to the ThirdCircle variable, which is of the
same class as SecondCircle .
Float Circle Blue X: 5.00 Y: 2.00 D: 10.00
Circle Blue X: 5 Y: 2 D: 10
FloatCircle.Assign(SecondCircle);
FloatCircle.Write;
ThirdCircle.Assign(FloatCircle);
ThirdCircle.Write;
The first exception is the result of assigning a Square to a Unicorn . The code,
as it is written, does not allow such assignments, even though TUnicorn is a
descendant of the TShape class. The code also does not allow Unicorn
assignments to Square .
EConvertError: Cannot assign a TSquare to a TUnicorn
EConvertError: Cannot assign a TUnicorn to a TSquare
try
Unicorn.Assign(Square);
except
on E: Exception do Writeln(E.ClassName, ': ', E.Message);
end;
try
Square.Assign(Unicorn);
except
on E: Exception do Writeln(E.ClassName, ': ', E.Message);
end;
The code does allow assigning one TUnicorn instance to another TUnicorn
instance:
Unicorn Pink X: 1 Y: 0 Magic Size: 3
Unicorn Pink X: 1 Y: 0 Magic Size: 3
Unicorn.Write;
PinkUnicorn.Assign(Unicorn);
PinkUnicorn.Write;
And the last two lines are the result of assigning to and from the
completely unrelated and unsupported TNotShape class. Those assignments
raise exceptions:
EConvertError: Cannot assign a TSquare to a TNotShape
EConvertError: Cannot assign a TNotShape to a TSquare
Let's create some simple classes and observe the differences between
using deep copied objects and shared object instances. TCustomText is a
base class that implements common fields and behavior. Its descendant
classes differ in how they treat their Style property. TStyledText keeps its
own copy of the Style instance, while TUniformText uses a shared instance.
That does not mean, however, that different TUniformText instances cannot
use different shared instances of TStyle .
uses
System.SysUtils,
System.Classes;
type
TStyle = class(TPersistent)
strict protected
FColor: string;
FSize: Integer;
public
constructor Create;
procedure Assign(Source: TPersistent); override;
property Color: string read FColor write FColor;
property Size: Integer read FSize write FSize;
end;
TCustomText = class(TObject)
strict protected
FText: string;
FStyle: TStyle;
public
constructor Create(const AText: string);
procedure Write;
end;
TStyledText = class(TCustomText)
strict protected
procedure SetStyle(const Value: TStyle);
public
property Text: string read FText write FText;
property Style: TStyle read FStyle write SetStyle;
end;
TUniformText = class(TCustomText)
public
property Text: string read FText write FText;
property Style: TStyle read FStyle write FStyle;
end;
constructor TStyle.Create;
begin
inherited;
FColor := 'Black';
FSize := 10;
end;
procedure TCustomText.Write;
begin
Writeln(FStyle.Color, ' Size: ', FStyle.Size, ' ', FText);
end;
var
RedStyle: TStyle;
Title, Text: TStyledText;
begin
Title := TStyledText.Create('Title');
Text := TStyledText.Create('Text');
Title.Write;
Text.Write;
RedStyle := TStyle.Create;
RedStyle.Color := 'Red';
RedStyle.Size := 12;
Title.Style := RedStyle;
Text.Style := RedStyle;
Title.Write;
Text.Write;
RedStyle.Color := 'Pink';
RedStyle.Size := 8;
Title.Write;
Text.Write;
Title.Style.Color := 'Orange';
Title.Write;
Text.Write;
end.
Let's explore the TStyledText class first. We start with a default style - black
color and size 10. Next we assign RedStyle - red color, size 12 - to both the
Title and Text objects. But the main feature of deep copied instances is
seen when we change the color of RedStyle to pink and set its size to 8.
Since Text and Title have their own copies, the changes in RedStyle had
absolutely no influence. Similarly, changing Title.Style.Color to orange only
changed the appearance of Title .
Black Size: 10 Title
Black Size: 10 Text
Red Size: 12 Title
Red Size: 12 Text
Red Size: 12 Title
Red Size: 12 Text
Orange Size: 12 Title
Red Size: 12 Text
All of the above examples can also be run in the classic compiler, but as-
is they would cause memory leaks because they do not have any of the
memory management related code required for the classic compiler.
Implementing the deep copy behavior for TStyledText in the classic compiler
is simple. To add the appropriate memory management, the TStyledText
destructor needs to be overridden and the owned FStyle field must be
released.
destructor TStyledText.Destroy;
begin
FStyle.Free;
inherited;
end;
While both deep copying and shared instances have their use cases
when you are dealing with scenarios that are similar to the above
mentioned styled text, imagine using shared instances to define car color
and decorations. You go crazy and paint your silver car pink and
suddenly every second car around you turns pink, too.
9 Releasing object instances
Releasing object instances is drastically different in the classic and
NextGen ARC compilers. On the surface, you can call the same methods
on both but that is where all similarities end. Underneath there are
completely different behaviors and implementations.
This chapter will cover only the classic compiler's behavior, coverage of
the ARC side is left for the ARC chapter.
9.1 Destroy
The destructor is the entry point for releasing any object instance. It is a
point of no return. Once the destructor is called, the object instance is on
its way to oblivion. The default destructor in Delphi is a virtual Destroy
method, implemented in TObject . It does not do anything in particular,
because TObject does not require any custom cleanup in its destructor.
TObject = class
public
...
destructor Destroy; virtual;
...
end;
destructor TObject.Destroy;
begin
end;
All the other mentioned methods for triggering object destruction will at
some point call Destroy .
But long, long, long before ARC compilers emerged rendering the above
code incompatible, there was an official recommendation to use Free
instead of Destroy - System.TObject.Destroy
Do not call Destroy directly. Call Free instead. Free verifies that the
object reference is not nil before calling Destroy.
That is one of the reasons you will rarely see the above
creation/destruction pattern in any Delphi codebase, if ever.
As to why, you will have to read further...
9.2 Free
is the foremost, fundamental method for releasing object instances,
Free
and as such it is declared in the root TObject class as a public static
method. The implementation below is specific to the classic Delphi
compiler. The ARC compiler translates calls to Free to mere nil
assignments.
TObject = class
public
...
procedure Free;
...
end;
procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;
Well, not exactly. Since Free is a static method, its address is resolved at
compile time based on the variable's type and you can safely call it even
if the object reference is nil . Self is also known inside the method's
scope, even though its value may be nil . So calling Free on a nil
reference is a perfectly safe thing to do.
And the most important thing... Using Free streamlines the code. It
alleviates the burden of thinking which one is appropriate when there are
no differences in code intent between Destroy and Free . Both say that you
no longer need an object and you want to release it. The fact that the
object may or may not be nil is irrelevant at that point.
9.3 DisposeOf
The DisposeOf method was introduced with Delphi XE4 and the first ARC
compiler. On classic compilers, DisposeOf will just make an inline call to
Free . Its primary purpose in the classic compiler is providing cross-
compiler code compatibility.
Even though it just maps to Free on the classic compiler, DisposeOf should
not be used as a casual replacement for Free . Its usage is only
appropriate if it is required by the ARC side of the code in question.
9.4 FreeAndNil
One way of releasing objects in Delphi is passing an object reference to
the FreeAndNil procedure. Technically, it is NilAndFree because the passed
reference is first nilled and then released.
procedure FreeAndNil(var Obj);
var
Temp: TObject;
begin
Temp := TObject(Obj);
Pointer(Obj) := nil;
Temp.Free;
end;
First, it is logical to name such a function FreeAndNil . If you look at the code
sequence it is meant to replace, it is exactly call Free first and then assign
nil . It is easy to assume what the function does - and the actual
background logic of nilling first and releasing second can be considered a
mere implementation detail.
Obj.Free;
Obj := nil;
As to why the actual implementation nils first and releases later, contrary
to popular belief, it is not because of thread safety. The current
implementation is not thread safe at all. Two threads can easily enter the
object destructor.
If you want to read more about the possible whys, Allen Bauer briefly
discussed it in his blog post, Exceptional Safety.
So far, so good. There is not much to be said about what the FreeAndNil
procedure does, it is pretty straightforward. But how and where it should
be used... well, that is something completely different. If you are feeling
bored you can always go to the Delphi forums, start a thread containing
FreeAndNil in the title, grab some popcorn and you will be entertained for
weeks to come.
There is nothing better for cleaning up the mess than sticking to the facts,
so I will not just plainly tell you when to use or not to use FreeAndNil (which
I eventually will...), but will also explain what FreeAndNil does and what it
does not do.
procedure DoFoo;
var
Foo: TFoo;
begin
Foo := TFoo.Create;
try
// do something with Foo
finally
Foo.Free;
end;
// accessing Foo beyond this point will result in undefined behavior
end;
The above code does not have any use for FreeAndNil . Of course, after Foo
is released and not nilled it will constitute a dangling reference, and
accessing it afterwards will result in undefined behavior.
There are several questions here. How often do you write such
erroneous code? How hard is it to spot such an error while reading the
code?
If using FreeAndNil really makes the difference, then you probably have a
different problem at hand. Keeping methods small enough to make code
easier to read and follow is a better option than using FreeAndNil just in
case.
For instance, when refactoring the following code from using TObject
var
Ref: TObject;
Ref := TObject.Create;
Ref.Free;
var
Ref: IInterface;
Ref := TInterfacedObject.Create;
Ref.Free;
Ref := TInterfacedObject.Create;
FreeAndNil(Ref);
nils one and only one reference. The one you passed as a
FreeAndNil
parameter. Any other references to the object will not be nilled and will
now represent invalid - stale - references to the object.
A slightly modified stale reference example that uses FreeAndNil is all the
proof we need that only a single reference will be set to nil : the one
passed as parameter to FreeAndNil , in this case Obj . The other reference to
our object instance, Ref , will not be set to nil by that call.
program StaleFreeAndNil;
uses
System.SysUtils,
System.Classes;
procedure Proc;
var
Obj, Ref: TObject;
Owner, Child: TComponent;
begin
Obj := TObject.Create;
Ref := Obj;
Writeln('Obj created, Ref assigned');
Writeln('Obj address ', NativeUInt(Obj), ', is nil ', Obj = nil, ',
assigned ', Assigned(Obj));
Writeln('Ref address ', NativeUInt(Ref), ', is nil ', Ref = nil, ',
assigned ', Assigned(Ref));
Writeln(Ref.ToString);
FreeAndNil(Obj);
// at this point, Ref is a stale pointer
Writeln('Obj released');
Writeln('Obj address ', NativeUInt(Obj), ', is nil ', Obj = nil, ',
assigned ', Assigned(Obj));
Writeln('Ref address ', NativeUInt(Ref), ', is nil ', Ref = nil, ',
assigned ', Assigned(Ref));
Writeln(Ref.ToString);
begin
Proc;
end.
This is yet another reason why using FreeAndNil as some sort of magical
safety mechanism is rather useless. Keeping strict ownership and not
taking and storing unnecessary object references are the only true
mechanisms for avoiding invalid reference issues.
9.4.5 Using a nil reference (as in calling methods on it) does not
always result in a crash
This is a rather amusing side effect. At least it is if you never had to track
down such buggy code... Accessing lazy initialized references during the
destruction process can resurrect them if they are accessed after they
have been released. This will happen regardless of whether you have
used FreeAndNil or released and nilled the reference in some other way.
So you think you have killed the object and bam, a few instructions later it
has been recreated because some other code tried to do something with
it.
program LazyFreeAndNil;
uses
System.SysUtils;
type
TLazy = class(TObject)
public
constructor Create;
destructor Destroy; override;
procedure SomeLazyWork;
end;
TLazyOperator = class(TObject)
private
FLazy: TLazy;
function GetLazy: TLazy;
public
destructor Destroy; override;
procedure WorkWithLazy;
property Lazy: TLazy read GetLazy;
end;
constructor TLazy.Create;
begin
inherited;
Writeln('Lazy lives');
end;
destructor TLazy.Destroy;
begin
Writeln('Lazy destroyed');
inherited;
end;
procedure TLazy.SomeLazyWork;
begin
Writeln('Some lazy Work');
end;
destructor TLazyOperator.Destroy;
begin
FreeAndNil(FLazy);
if Lazy = nil then Writeln('Lazy is no more')
else Writeln('Alive and kicking');
inherited;
end;
procedure TLazyOperator.WorkWithLazy;
begin
Lazy.SomeLazyWork;
end;
var
Worker: TLazyOperator;
begin
ReportMemoryLeaksOnShutdown := true;
Worker := TLazyOperator.Create;
try
Worker.WorkWithLazy;
finally
Worker.Free;
end;
end.
When we run the above example we will get the following output and it
will leak one TLazy instance.
Lazy lives
Some lazy work
Lazy destroyed
Lazy lives
Alive and kicking
The first three lines are expected, but the next two are not. Accidentally,
in an attempt to prove that we successfully destroyed a TLazy object
instance we directly accessed the Lazy property instead of the FLazy field.
And accessing the property automatically recreated the object.
Fixing the line if Lazy = nil then Writeln('Lazy is no more') by replacing Lazy with
FLazy in nil check will behave as expected, producing the following output:
Lazy lives
Some lazy work
Lazy destroyed
Lazy is no more
FreeAndNil should be used when a nil value stored in some variable is used
as a flag value, and the code logic follows different execution paths
depending on whether or not that variable contains nil . That means
FreeAndNil should be used on a reference only and only if usage of that
reference is accompanied by nil checks.
However, that does not mean that you should sprinkle your code with nil
checks to avoid accidental errors or merely to satisfy the above condition.
To rephrase, if a reference can at any point hold nil as a valid value and
depending on that flag value, determined by a nil check, you may or may
not execute some code as part of your intended workflow then you must
release such a reference using FreeAndNil or use the Free and nil
assignment sequence. FreeAndNil is not necessary only if such a
reference, once created, will remain alive as long as it remains in scope.
This is the only valid use case for FreeAndNil or any other method that nils
the reference after release.
destructor TLazyOperator.Destroy;
begin
FLazy.Free; // we can safely use Free here
inherited;
end;
If you are aware of all the things FreeAndNil does not do, why not just use it
everywhere instead of Free ? You know what it does, and surely it might
occasionally save you from silly mistakes. Why is unifying the code by
using Free instead of the Destroy/Free combination a good thing, while taking
one step further and unifying the code with FreeAndNil is a bad thing?
The first reason is type safety, where you are making a tradeoff between
compile time errors and runtime errors. When type safety is concerned
there is no difference between Destroy and Free . In that regard, using
FreeAndNil solely because it can prevent you from making one silly mistake
while opening up doors for making a different silly mistake is probably not
the best reason.
The next reason, and this is the important one, is that FreeAndNil and Free
are different in intent. FreeAndNil carries additional intent and it says this
variable is meant to be reused. Or you are dealing with a complex object
instance with a convoluted destruction process and using FreeAndNil on its
inner objects can prevent issues coming from outside factors beyond
your control - moreover having such complicated destruction code can be
an indication of overall bad design.
Using FreeAndNil in the wrong places gives the wrong signals to people
reading and using the code. There are tools to be used for debugging
purposes, FreeAndNil is not such a tool. It pollutes code with unnecessary
calls that obscure the code's purpose and meaning, all for the purpose of
catching some obscure bug that might happen if the developer is not
paying attention. The time spent understanding such code can easily be
greater than the time spent once in a lifetime chasing some bug that
could, purely by chance, be found sooner if you had used FreeAndNil .
When all the above is considered, FreeAndNil brings way too few bug
protection benefits to be abused as some sort of safety net. It is not
particularly safe nor is it wide enough to catch many omissions.
10 Releasing through ownership
models
There is no magic involved when it comes to releasing object instances
through ownership models. At some point, code will still have to explicitly
call basic methods for releasing owned instances. The reason that this
kind of management deserves more detailed coverage is the fact that this
pattern is widely used not only in many Delphi core classes, but also as a
general coding pattern and developers have to know the general
principles involved in handling instances stored inside such classes, as
well as their common coding patterns.
What does the ownership model actually represent here? After all, any
object instance owns its inner fields and is responsible for managing their
memory if applicable.
var
List: array of TObject;
Obj: TObject;
i, n: integer;
begin
ReportMemoryLeaksOnShutdown := true;
// initialize and populate the list
n := 3;
SetLength(List, n);
for i := 0 to n - 1 do
begin
Obj := TObject.Create;
List[i] := Obj;
end;
type
TArrayList = class(TObject)
private
FItems: array of TObject;
function GetItem(index: Integer): TObject;
public
destructor Destroy; override;
procedure Add(Item: TObject);
procedure Clear;
property Items[index: Integer]: TObject read GetItem;
end;
destructor TArrayList.Destroy;
begin
Clear;
inherited;
end;
procedure TArrayList.Clear;
var
i: Integer;
begin
for i := 0 to High(FItems) do
FItems[i].Free;
SetLength(FItems, 0);
end;
var
List: TArrayList;
Obj: TObject;
i: Integer;
begin
ReportMemoryLeaksOnShutdown := true;
Generally, before using any kind of collections for storing object instances
it is prudent to learn about their features and ownership model. There is a
great deal of variation of features and behavior even between the core
Delphi classes, not to mention third party libraries. Assuming specific
behavior without actually knowing how a specific collection works is the
fastest way of shooting yourself in the foot. Unless you love to live
dangerously, reading the documentation or code is mandatory.
10.1.1 System.Contnrs.TObjectList
The first, parameterless constructor will set the OwnsObject property to True
and take ownership of contained objects. The second constructor allows
us to configure the OwnsObject property - passing True will have the same
effect as using the first constructor, while passing False will not maintain
ownership of contained object instances.
The value of OwnsObject is used in the Clear , Remove and RemoveItem methods. If
OwnsObject is True when the method is called, the object instance(s) will be
released. On the other hand, the Extract and ExtractItem methods will return
a specific object instance after removing it from the list, but without
releasing it - even if the collection owns the contained object instances -
this is another place where ownership transfer happens, but in this case
from the collection to the caller.
We will now take a deeper look at how the ownership model in TObjectList
works, and all the variations we can use, depending on our needs.
For a start, three freshly created number objects will be added to the list
in a loop, then we will output the number of objects in the list, and then
iterate through the list and write the values stored in each object
instance.
program ObjectList;
uses
System.Classes,
System.Contnrs;
type
TNumber = class(TObject)
public
Value: integer;
constructor Create(ANumber: integer);
end;
var
List: TObjectList;
Number: TNumber;
i: integer;
begin
ReportMemoryLeaksOnShutdown := true;
List := TObjectList.Create;
try
for i := 0 to 2 do
begin
Number := TNumber.Create(i + 1);
List.Add(Number);
end;
WriteList(List);
finally
List.Free;
end;
end.
Running the code will produce the following output and no memory leaks
will be reported:
Count: 3
Number at position: 0 is 1
Number at position: 1 is 2
Number at position: 2 is 3
The following piece of code creates and releases our List of objects. The
code calls the parameterless constructor, which implies ownership of the
objects added to the list. That is why there are no memory leaks, even
though we never explicitly call Free on any Number object instance.
TObjectList 's destructor calls the Clear method that will clear the list and
handle owned objects - if they are owned.
List := TObjectList.Create;
try
...
finally
List.Free;
end;
Creating List with the other constructor and passing False to its AOwnsObjects
parameter will result in three leaked TNumber instances.
List := TObjectList.Create(false);
Now, let's take a look at the first loop that creates and adds numbers to
the list.
for i := 0 to 2 do
begin
Number := TNumber.Create(i + 1);
List.Add(Number);
end;
We can still use Number to access the object instance since it is a valid
reference, but since we have transferred ownership, any changes in the
List might convert our Number into a stale reference.
Back to the loop. After we added the object instance to the list, we can
reuse the Number variable for storing another TNumber instance created in the
next loop iteration.
One of the common mistakes made by developers not yet fully
acquainted with manual memory management is calling Free after adding
Number to the list, because they have been told to release objects they
have created, but they are not aware they have transferred ownership of
the object to the list. The following code would result in a crash when we
call List.Free . In the meantime, any attempt to access objects stored in the
list may or may not seem to work properly, depending on the code.
for i := 0 to 2 do
begin
Number := TNumber.Create(i + 1);
List.Add(Number);
// THIS IS WRONG
// This line would result in stale references inside TList
Number.Free;
end;
If we combine the above erroneous code with a list that does not hold
ownership of the object instances, we can create a non-crashing, non-
leaking example that produces the wrong output.
List := TObjectList.Create(false);
try
for i := 0 to 2 do
begin
Number := TNumber.Create(i + 1);
List.Add(Number);
Number.Free;
end;
WriteList(List);
finally
List.Free;
end;
And instead of the correct output (the one from the original example) we
would get
Count: 3
Number at position: 0 is 3
Number at position: 1 is 3
Number at position: 2 is 3
The following example will show how the ownership model works when
we remove or extract items from the list. We will fill the list with number
objects as before, and then we will remove odd numbers from the list.
Since we are removing items from the list while iterating through it, the
second loop will iterate through the list backwards, or we would have to
use a while loop.
The Remove method also releases the object instance if the list holds
ownership, so there will be no memory leaks.
List := TObjectList.Create;
try
for i := 0 to 2 do
begin
Number := TNumber.Create(i + 1);
List.Add(Number);
end;
WriteList(List);
WriteList(List);
finally
List.Free;
end;
Count: 3
Number at position: 0 is 1
Number at position: 1 is 2
Number at position: 2 is 3
Count: 1
Number at position: 0 is 2
begin
ReportMemoryLeaksOnShutdown := true;
OddList := nil;
List := TObjectList.Create;
try
OddList := TObjectList.Create;
for i := 0 to 2 do
begin
Number := TNumber.Create(i + 1);
List.Add(Number);
end;
WriteList(List);
i := 0;
while i < List.Count do
begin
Number := TNumber(List.Items[i]);
if Odd(Number.Value) then OddList.Add(List.Extract(Number))
else Inc(i);
end;
WriteList(List);
WriteList(OddList);
finally
OddList.Free;
List.Free;
end;
end.
Count: 3
Number at position: 0 is 1
Number at position: 1 is 2
Number at position: 2 is 3
Count: 1
Number at position: 0 is 2
Count: 2
Number at position: 0 is 1
Number at position: 1 is 3
for i := 0 to 2 do
begin
Number := TNumber.Create(i + 1);
List.Add(Number);
end;
WriteList(List);
for i := 0 to List.Count - 1 do
begin
Number := TNumber(List.Items[i]);
if Odd(Number.Value) then OddList.Add(Number);
end;
WriteList(List);
WriteList(OddList);
finally
OddList.Free;
List.Free;
end;
Count: 3
Number at position: 0 is 1
Number at position: 1 is 2
Number at position: 2 is 3
Count: 3
Number at position: 0 is 1
Number at position: 1 is 2
Number at position: 2 is 3
Count: 2
Number at position: 0 is 1
Number at position: 1 is 3
10.1.2 System.Contnrs.TComponentList
uses
System.SysUtils,
System.Classes,
System.Contnrs;
var
List: TComponentList;
Component: TComponent;
i: integer;
begin
ReportMemoryLeaksOnShutdown := true;
List := TComponentList.Create;
try
for i := 0 to 2 do
begin
Component := TComponent.Create(nil);
Component.Name := 'Component' + IntToStr(i + 1);
List.Add(Component);
end;
WriteList(List);
Readln;
end.
Running the above example will produce the following output and no
leaks, even though we have created components without an owner.
Adding components to the list will take ownership over the component.
Calling Free explicitly on any component in the list will notify the list of its
removal and there would be no stale references left in the list.
Count: 3
Component at position: 0 is Component1
Component at position: 1 is Component2
Component at position: 2 is Component3
Count: 2
Component at position: 0 is Component1
Component at position: 1 is Component2
uses
System.Classes,
System.Contnrs;
var
List: TObjectList;
Component: TComponent;
i: integer;
begin
ReportMemoryLeaksOnShutdown := true;
List := TObjectList.Create;
try
for i := 0 to 2 do
begin
Component := TComponent.Create(nil);
List.Add(Component);
end;
Component.Free;
finally
List.Free;
end;
end.
In the above example, a crash will happen at the List.Free line, but only
because the list contains a stale reference to a component we explicitly
released with Component.Free .
10.1.3 System.Generics.Collections.TObjectList<T>
uses
System.Classes,
System.Generics.Collections;
var
List, OddList: TObjectList<TNumber>;
Number: TNumber;
i: integer;
begin
ReportMemoryLeaksOnShutdown := true;
OddList := nil;
List := TObjectList<TNumber>.Create;
try
OddList := TObjectList<TNumber>.Create;
for i := 0 to 2 do
begin
Number := TNumber.Create(i + 1);
List.Add(Number);
end;
WriteList(List);
i := 0;
while i < List.Count do
begin
Number := List.Items[i];
if Odd(Number.Value) then OddList.Add(List.Extract(Number))
else Inc(i);
end;
WriteList(List);
WriteList(OddList);
finally
OddList.Free;
List.Free;
end;
end.
10.1.4 System.Generics.Collections.TObjectStack<T>
Using the Push method transfers ownership of the object instance to the
stack. After the instance is pushed it may become a stale reference if the
contents of the stack are altered in any way. So we have to be careful
about how we use such a reference later on. The best practice is not to
use them at all after we have transferred ownership. By doing so, we
reduce the chance of accidental errors in subsequent code changes.
There are several methods for retrieving objects pushed to the stack. Peek
will return the top object instance but without removing it from the stack -
the stack will still hold ownership over it. Such a peeked reference should
be used locally and with care, because it will be valid only for as long as
the stack is not altered. Pop - will just remove the top instance, and
release it if the stack owns instances. And Extract will return the top
instance, removing it from the stack, but without releasing it. We have to
release the returned value manually.
program GenericStack;
uses
System.Classes,
System.Generics.Collections;
var
Stack: TObjectStack<TNumber>;
Number: TNumber;
i: integer;
begin
ReportMemoryLeaksOnShutdown := true;
Stack := TObjectStack<TNumber>.Create;
try
for i := 0 to 2 do
begin
Number := TNumber.Create(i + 1);
Stack.Push(Number);
end;
Writeln('Count: ', Stack.Count);
Number := Stack.Extract;
Writeln('Count: ', Stack.Count);
Writeln('Extracted number is: ', Number.Value);
Number.Free;
Stack.Pop;
Writeln('Count: ', Stack.Count);
finally
Stack.Free;
end;
end.
Count: 3
Count: 2
Extracted number is: 3
Count: 1
10.1.5 System.Generics.Collections.TObjectQueue<T>
Just as the generic stack has a variant with ownership, so does the
generic queue. Again, with an OwnsObjects property configurable through the
constructor. Just as with stacks, if a queue owns contained instances
they will be released when the queue gets released.
Using the Enqueue method, we add an object instance and transfer its
ownership to the queue. The same rules apply for making sure we don't
use added objects after we transferred ownership to the queue to avoid
using stale references.
The queue also has several methods for retrieving objects. Peek will return
the head object instance but without removing it from the queue - the
queue will still hold ownership on it. Dequeue - will just remove the head
instance and release it if the queue owns instances. And Extract will return
the head instance, removing it from the queue, but without releasing it.
We have to release the returned value manually.
program GenericQueue;
uses
System.Classes,
System.Generics.Collections;
var
Queue: TObjectQueue<TNumber>;
Number: TNumber;
i: integer;
begin
ReportMemoryLeaksOnShutdown := true;
Queue := TObjectQueue<TNumber>.Create;
try
for i := 0 to 2 do
begin
Number := TNumber.Create(i + 1);
Queue.Enqueue(Number);
end;
Writeln('Count: ', Queue.Count);
Number := Queue.Extract;
Writeln('Count: ', Queue.Count);
Writeln('Extracted number is: ', Number.Value);
Number.Free;
Queue.Dequeue;
Writeln('Count: ', Queue.Count);
finally
Queue.Free;
end;
end.
Count: 3
Count: 2
Extracted number is: 1
Count: 1
10.1.6
System.Generics.Collections.TObjectDictionary<TKey,TValue>
var
Dictionary: TObjectDictionary<string, TNumber>;
Number: TNumber;
Pair: TPair<string, TNumber>;
begin
ReportMemoryLeaksOnShutdown := true;
Number := TNumber.Create(2);
Dictionary.Add('two', Number);
Number := TNumber.Create(3);
Dictionary.Add('three', Number);
WriteDictionary(Dictionary);
Number := TNumber.Create(33);
Dictionary.AddOrSetValue('three', Number);
Dictionary.Remove('two');
WriteDictionary(Dictionary);
Pair := Dictionary.ExtractPair('one');
Pair.Value.Free;
WriteDictionary(Dictionary);
finally
Dictionary.Free;
end;
end.
Count: 3
Key: one contains value: 1
Key: two contains value: 2
Key: three contains value: 3
Count: 2
Key: one contains value: 1
Key: three contains value: 33
Count: 1
Key: three contains value: 33
In the above example, the dictionary will only take ownership of its
values, because the keys are declared as string and are not a class type.
Taking ownership of any non-class type will result in an EInvalidCast
exception raised at runtime. You can test that by replacing the above
dictionary construction with the following code:
Dictionary := TObjectDictionary<string, TNumber>.Create([doOwnsKeys, doOwnsValu
Even if you use class types for keys or values, you don't have to maintain
ownership through the dictionary. But in that case, all object instances
added to the dictionary must be handled either manually, or by storing
them in some other collection that will take care of their release by
owning them.
Running the following example will leak a single TKeyNumber instance, the
one we tried to add with AddOrSetValue . If we create that key with a non-
existent value, like 13, TKeyNumber.Create(13) , there will be no leak.
program GenericDictionaryKeys;
uses
System.Classes,
System.Generics.Defaults,
System.Generics.Collections;
type
TNumber = class(TObject)
public
Value: integer;
constructor Create(ANumber: integer);
end;
TKeyNumber = class(TNumber);
procedure Test;
var
Dictionary: TObjectDictionary<TKeyNumber, TNumber>;
Key: TKeyNumber;
Number: TNumber;
Comparer: IEqualityComparer<TKeyNumber>;
i: integer;
begin
Comparer := TDelegatedEqualityComparer<TKeyNumber>.Create(
Key := TKeyNumber.Create(3);
Number := TNumber.Create(33);
Dictionary.AddOrSetValue(Key, Number);
finally
Dictionary.Free;
end;
end;
begin
ReportMemoryLeaksOnShutdown := true;
Test;
end.
We have to replace
Dictionary.AddOrSetValue(Key, Number);
with
if Dictionary.ContainsKey(Key) then
begin
Dictionary.AddOrSetValue(Key, Number);
Key.Free;
end
else Dictionary.Add(Key, Number);
If Dictionary already contains the Key , we can release our newly created -
duplicate - Key after we add Number to the Dictionary .
There is another thing to keep in mind when two objects are created to
be added. Standard construction protection with a twist - since we will
use a try...except block with a re-raising exception. If constructing the
second object fails - we have to release the first one or it will leak.
Key := TKeyNumber.Create(i);
try
Number := TNumber.Create(i);
except
Key.Free;
raise;
end;
And that is not all... there are more possible leaks in the above code...
ones caused by ownership transfer failure that can happen not just in a
dictionary, but in any collection. Coming up next... but before we dive into
such exception handling, let's cover another core collection type.
10.1.7 System.Classes.TCollection
The functions Add and Insert are actually factory functions that construct a
new collection item, add it to the collection, and return it as result,
primarily for customization purposes. There is no external ownership
transfer here, as collection items are automatically added to the collection
within the collection instance. This happens strictly internally as TCollection
delegates maintaining its items to a generic list.
At first glance it seems that you cannot move items between collections
or even extract them, but that functionality is publicly available through
TCollectionItem , which has a Collection property pointing back to its owner
collection. By changing that property, we can either move an item to
another collection or extract it from the collection if we set Collection to nil .
The extracted item has to be manually released or a memory leak will
occur.
The following example shows basic usage of the mentioned methods and
properties.
program Collection;
uses
System.SysUtils,
System.Classes;
type
TNumberItem = class(TCollectionItem)
protected
function GetDisplayName: string; override;
public
Value: Integer;
end;
function TNumberItem.GetDisplayName: string;
begin
Result := IntToStr(Value);
end;
var
List: TCollection;
Item: TCollectionItem;
i: integer;
begin
ReportMemoryLeaksOnShutdown := true;
List := TCollection.Create(TNumberItem);
try
for i := 0 to 2 do
begin
TNumberItem(List.Add).Value := i + 1;
end;
WriteList(List);
List.Delete(1);
WriteList(List);
Item := TNumberItem(List.Items[0]);
Item.Collection := nil;
WriteList(List);
Item.Free;
finally
List.Free;
end;
end.
Count: 3
Item at position: 0 is 1
Item at position: 1 is 2
Item at position: 2 is 3
Count: 2
Item at position: 0 is 1
Item at position: 1 is 3
Count: 1
Item at position: 0 is 3
The above code is technically, absolutely the most correct code you can
write. Still, you will find that such situations are not handled properly in
many code-bases.
Why?
It all depends on the what a particular piece of code is doing and where.
Inability to add an object to the list due to an out-of-memory exception is
a pretty serious issue. It usually means something is horribly wrong, the
application is going to hell anyway and taking care of a small leak seems
like an exercise in futility. And an additional try...except block does not
improve code readability.
If the parser function properly cleans up all the pieces, and the
application captures and handles the exception, it can be a fully
recoverable error. The application can just show the user that that
particular file could not be processed because it is too large, and that is it.
It does not have to go down in flames, it can continue to function
perfectly, fully capable of parsing some smaller files.
If the parser function did not properly clean up memory, that small object
it allowed to leak could end up sitting in the middle of the heap, causing
memory fragmentation. Repeated leaks can severely limit the
application's ability to function properly until even much smaller files can
no longer be successfully processed.
After having one eggnog too many, feeling exceptionally merry, you make
a New Year's resolution. You will clear every possible leak in your code.
You are not going to be a sloppy programmer. And you start digging into
your code, adding all the missing try...except blocks.
And then you bump into this one. Grumble... this is getting more and
more convoluted.
i := 0;
while i < List.Count do
begin
Number := List.Items[i];
if Odd(Number.Value) then OddList.Add(List.Extract(Number))
else Inc(i);
end;
Do you add the object back? Do you add it first and then extract? Do you
just Free the darn thing?
i := 0;
while i < List.Count do
begin
Number := List.Items[i];
if Odd(Number.Value) then
begin
OddList.Add(Number);
List.Extract(Number);
end
else Inc(i);
end;
i := 0;
while i < List.Count do
begin
Number := List.Items[i];
if Odd(Number.Value) then
try
OddList.Add(List.Extract(Number));
except
Number.Free;
raise;
end
else Inc(i);
end;
Adding the object back makes no sense. And when choosing between an
add-extract sequence and catching the exception, the former produces
cleaner code. Cleaner??? Well... cleaner in the sense that there are
fewer lines of code. Its intent, on the other hand, may be not so clear.
Anyone reading that code would have to be aware that you are trying to
avoid a memory leak in the event of a total calamity, so adding a
comment to the code might be wise thing to do.
First, a simple situation when only values are owned by the dictionary.
This is pretty much the same situation as with a plain object list. Both Add
or AddOrSetValue can fail and we have to avoid the leak by releasing the
value instance inside a try...except block. Basically, what we should do
with any other collection using any other similar method that adds an
instance to any collection. But, let's repeat that code anyway.
Number := TNumber.Create(1);
try
Dictionary.Add('one', Number);
except
Number.Free;
raise;
end;
If we want to extract a value from one dictionary and add it to another
dictionary or collection we will be in a situation similar to the previously-
mentioned example of moving an object from one list to another.
Dictionaries that also own their keys are the really problematic ones. We
already covered a situation where Key must be released if owned by the
dictionary, but that code didn't handle possible exceptions during
ownership transfer. The following code handles them all. If you think that
execution of a branch where Key is already contained in the collection
cannot fail and that you don't have to protect AddOrSetValue in this particular
situation, you are wrong. Under the right circumstances it can also fail,
because that method will first try to grow the dictionary, and then it will
use the appropriate code for adding the value depending on whether the
key already exists.
Key := TKeyNumber.Create(i);
try
Number := TNumber.Create(i);
except
Key.Free;
raise;
end;
if Dictionary.ContainsKey(Key) then
try
try
Dictionary.AddOrSetValue(Key, Number);
except
Number.Free;
raise;
end
finally
Key.Free;
end
else
try
Dictionary.Add(Key, Number);
except
Key.Free;
Number.Free;
raise;
end;
If you think the above is a rather convoluted piece of code, trust me, you
are not the only one.
When it comes to the TCollection class, the situation is slightly different.
As you might guess, using the Collection property to extract an item from a
collection is a perfectly safe operation, just like any other extraction,
however using Collection to move an item into another collection is not.
The SetCollection method used to change the Collection property has the
following implementation:
procedure TCollectionItem.SetCollection(Value: TCollection);
begin
if FCollection <> Value then
begin
if FCollection <> nil then FCollection.RemoveItem(Self);
if Value <> nil then Value.InsertItem(Self);
end;
end;
If InsertItem in the above code fails, the collection item's ownership transfer
will not be completed and we will have to take care of that item to prevent
it from leaking.
Item := FirstCollection.Items[0];
try
Item.Collection := SecondCollection;
except
Item.Free;
raise;
end;
Avoiding such ownership transfer leaks under manual memory
management usually implies knowing the internals of the used classes.
Internals you normally should not have to manage. Once again, a deeper
knowledge of the used libraries and frameworks is the only way to
prevent issues that must be solved in your own code.
When you deal with any kind of ownership transfer, no matter how
innocent your side of the code looks, it is the code the behind the scenes
that might fail and cause you some troubles.
This brings us back to your code-fixing story which has two possible
endings, neither of which are exceptionally happy...
You don't fulfill your New Year's resolution - who can blame you?
You fulfill your New Year's resolution and make a new one, never to make
such a stupid resolution again!
In any case, you decide ARC is not such a bad thing after all and you
become an avid proponent ;-)
Each TComponent object instance can own any number of other components
maintained in the internal FComponents list. The accompanying public read-
only properties ComponentCount and Components can be used for iterating
through owned components. When a component is destroyed, all
components in its FComponents list will also be destroyed.
try
NewOwner.InsertComponent(Child);
except
Child.Free;
raise;
end;
There are two common patterns used when constructing and releasing a
temporary component:
procedure TFooComponent.DoSomething;
var
Local: TComponent;
begin
Local := TComponent.Create(nil);
try
// do something with component
finally
Local.Free;
end;
end;
procedure TFooComponent.DoSomething;
var
Local: TComponent;
begin
Local := TComponent.Create(Self);
try
// do something with component
finally
Local.Free;
end;
end;
The first pattern, the one without an owner, is preferred because it clearly
indicates the intent that the component is just a temporary local one. It
also skips unnecessary adding and removing component from its owner's
list.
The second pattern, where the temporary local component has an owner,
has many variations where different owners are used. Sometimes
Application is used, sometimes a specific form variable, sometimes even
Self.Owner . Besides the fact that accessing any global variable from the
method represents a bad coding pattern, the fact that you can actually
pass just about any component as an owner proves that this is really an
anti-pattern.
There is also a third solution that does not release the temporary local
component at the end of the method. This is bad code, because it will not
create a memory leak in the classic sense, but this temporary component
will not be released until its owning component is released. Effectively,
calling that method over and over again will continually add new Local
components to the TFooComponent instance's owned components list, wasting
memory and may eventually result in an out-of-memory exception.
This kind of code can be used only if we need to keep the locally
constructed component alive longer than the method's scope.
procedure TFooComponent.DoSomething;
var
Local: TComponent;
begin
Local := TComponent.Create(Self);
// do something with component
end;
type
TEngine = class(TComponent)
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
TCar = class(TComponent)
strict protected
FEngine: TEngine;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure Drive;
end;
destructor TEngine.Destroy;
begin
Writeln('Engine destroyed');
inherited;
end;
destructor TCar.Destroy;
begin
FEngine.Free;
inherited;
end;
procedure TCar.Drive;
begin
Writeln('Driving');
end;
var
Car: TCar;
begin
ReportMemoryLeaksOnShutdown := true;
Car := TCar.Create(nil);
try
Car.Drive;
finally
Car.Free;
end;
end.
Running the above code produces the following output, proving that the
inner engine object instance is properly created and destroyed:
Engine created
Driving
Engine destroyed
Changing the TCar constructor by passing Self as the owner of the inner
field and completely removing the TCar destructor will produce the same
output:
constructor TCar.Create(AOwner: TComponent);
begin
inherited;
FEngine := TEngine.Create(Self);
end;
When we use Self , our inner field component is referenced not by one,
but by two references. One is our field variable, and the other is the
reference stored in the owned component list. Moreover, since the owned
component list is constructed on an as-needed basis, even a single inner
field will result in the construction of the owned components list that will
consume both additional time and memory. The process of inserting and
removing the component from the owned list also takes time.
The TComponent class is pretty heavy either way, so counting bytes and
nanoseconds might look like premature optimization - but unless you
have a specific reason to use Self as an inner field owner, using nil is still
a good idea.
Label does not own its FocusControl , it is merely a collaborator object that
will be used only if it is set.
Self represents some component that can take ownership of the TEdit
instance, presumably a form, or we have to make sure that
Label1.FocusControl.Free will always be executed to prevent memory leaks.
Label1.FocusControl := TEdit.Create(Self);
...
Label1.FocusControl.Free;
The following example will assist us with exploring the inner workings of
the TComponent notification system. This could also be done by looking at
various VCL or FMX classes that have optional components, but stepping
through that code could be more tedious. Another advantage of a
separate example is providing basic template code for using the
notification system in your own custom components or controls.
uses
System.Classes;
type
TMaterial = (Wood, Stone, Metal);
TMagicWand = class(TComponent)
public
Material: TMaterial;
procedure Magic;
function MaterialToString: string;
end;
TWizard = class(TComponent)
strict protected
FWand: TMagicWand;
procedure SetWand(const Value: TMagicWand);
procedure Notification(AComponent: TComponent; Operation: TOperation); over
public
procedure UseWand;
published
property Wand: TMagicWand read FWand write SetWand;
end;
procedure TMagicWand.Magic;
begin
case Material of
Wood : Writeln('Casting magic with wooden wand');
Stone : Writeln('Casting magic with stone wand');
Metal : Writeln('Casting magic with metal wand');
end;
end;
procedure TWizard.UseWand;
begin
if Assigned(FWand) then FWand.Magic
else Writeln('Cannot cast magic without a wand');
end;
var
Wizard: TWizard;
WoodenWand, MetalWand: TMagicWand;
begin
ReportMemoryLeaksOnShutdown := true;
// creating wizard
Wizard := TWizard.Create(nil);
// clean up
MetalWand.Free;
Wizard.Free;
end.
Running the above code will result in the following output.
Cannot cast magic without a wand
Wand picked up: metal
Casting magic with metal wand
Dropping wand: metal
Wand picked up: wood
Casting magic with wooden wand
Wand has been destroyed: wood
Cannot cast magic without a wand
Let's start our code exploration with the UseWand method. It is not related to
the notification system, but to handling optional fields, in this case FWand
and the related property Wand . Since Wand is optional we must check it for
nil before we can safely use it. This is a common pattern with optional
fields or any other kind of optional variables. In the above code, the
UseWand method also has an else branch implemented - if Wand is nil , it prints
the appropriate message - but usually when dealing with optional fields
we would have just implemented the if branch, because there would be
nothing we must do in the else branch.
In the setter, we first remove ourselves from the current FWand free
notification list by calling RemoveFreeNotification(Self) , then we set the new
FWand and add ourselves to that wand's notification list. Of course, since
the wand could be nil , before we call any of the abovementioned
methods we must make sure that FWand is not nil .
procedure TWizard.SetWand(const Value: TMagicWand);
begin
if Assigned(FWand) then FWand.RemoveFreeNotification(Self);
FWand := Value;
if Assigned(FWand) then FWand.FreeNotification(Self);
end;
To make sure we are dealing with the right component we also have to
check whether the removed AComponent is the one we are looking for - in
this particular case, FWand . In our example, when the wooden wand is
destroyed in a fire, this piece of code is responsible for removing that
wand from the wizard, preventing access to an invalid reference. That is
why calling UseWand afterwards will not result in a crash or undefined
behavior, but will instead print Cannot cast magic without a wand.
If by any chance the wizard were holding the metal wand while the
wooden wand was being destroyed, the wizard would not be notified of
that destruction.
procedure TWizard.Notification(AComponent: TComponent; Operation: TOperation);
begin
inherited;
if Operation = opRemove then
begin
if AComponent = FWand then FWand := nil;
end;
end;
So far, the above Notification method does not look complicated at all. But,
if we extend our TWizard class and add additional optional properties, it will
get longer in no time.
TWizard = class(TComponent)
strict protected
FWand: TMagicWand;
FShield: TShield;
FBag: TBag;
...
end;
If you are designing some piece of code from scratch, it would be prudent
not to rely on TComponent notification as a weak reference model, unless you
are extending Delphi visual frameworks or making any kind of design
time system - in such a case you would be bound by the existing design,
where TComponent serves as base class.
The first flaw is that you cannot just take a reference to some component
and be done with it. You have to manually add the appropriate code so
you would be notified when you can no longer use that reference. You
also have to manually add code that will nil that reference. So in order to
have a fully functional notification system there is a whole lot of
boilerplate code you have to write in setters as well as the Notification
method.
With automatic reference counting in place, all that elaborate code could
be replaced with a single [weak] attribute. When the original object
instance is destroyed, all [weak] references pointing to it will be
automatically nilled (zeroed) and we would not have to take care of that
ourselves.
[weak] FWand: TWand;
And the next and more fatal flaw, is that the TComponent notification system
undermines ARC. It is not designed for the ARC compiler and the
combination of the two is not exactly working smoothly. You can read
more about it in ARC chapter, specifically in DisposeOf .
Anyway, if you plan to write any cross-platform code, you should avoid
using TComponent and its notification system as much as possible.
11 Automatic Reference Counting -
Overview
ARC stands for Automatic Reference Counting. It is a form of automatic
memory management system where the usage of each object reference
is tracked - counted - and an object instance is automatically released
when it is no longer reachable through any strong reference (see below) -
in other words, when its reference count falls down to zero.
This chapter, along with the next one, are introductory chapters intended
to give a general overview on ARC concepts and terminology as well as
the origins of ARC in Delphi. A deeper coverage of those concepts,
features and specific behaviors in both classic and NextGen ARC
compilers can be found later on, starting with the ARC - Concepts
chapter.
What is the significance of the above rule, how can you count on it if it
can be broken? If it is not actually true all the time, how can it even be
considered a rule, let alone a golden one?
It is not that every reference will be either nil or valid, but that you expect
them to be. What is the difference between expecting valid references in
ARC compared to expecting valid references in manual memory
management? You certainly don't want to use invalid references
anywhere.
In ARC, you start with all references initialized to nil , when you create
your object instances you deal with valid objects until they either go out of
scope or you explicitly nil them. By default, all ARC references follow the
golden rule of ARC. You have to explicitly create possible invalid
references by writing unsafe code.
Knowing that you are dealing with valid or nil references, greatly
streamlines the coding process and increases safety. Instead of having
potential invalid references running around, you have them confined in
safe corners where you can more easily keep them under control.
The [weak] attribute marks a zeroed weak reference. When the referenced
object is destroyed, zeroed weak references will be automatically set to
nil . This is a very important feature, because it prevents dangling
references and enforces the Golden rule that any reference to an object
instance must be either a valid reference or nil . We can safely use [weak]
as a starting point for marking weak references because we cannot go
wrong if we use the [weak] attribute on any weak reference we can
possibly have.
If the [weak] attribute is safe to use and does not leave dangling
references, why on Earth would we need anything else?
[unsafe] to the rescue (kind of). For references marked with the [unsafe]
attribute, the compiler does not generate any reference counting and
tracking code. But on the other hand, while using [unsafe] , you have to be
wary of dangling references. Because [unsafe] is reference counting
without a safety belt, and as such violates the Golden rule, we have to be
very careful how and where we use it.
11.3.3 Pointer
Storing a reference as a pointer works the same way as using the [unsafe]
attribute, but with one significant disadvantage - in order to use that
pointer, we have to use typecasting. Where possible, using [unsafe] is
preferred to using pointers.
Raw pointers do have one significant use case, though. All other
references stored in arrays are strong ones. If you need to maintain list of
non-zeroed weak references, an array of pointers is the simplest viable
solution. On the other hand, maintaining list of zeroed weak references
requires more complex solutions - wrapping weak references and storing
wrappers instead direct references in array.
Since the classic compiler does not have reference counting for object
references, they will act as raw pointers as far as reference counting is
concerned and will not trigger the reference counting mechanism. Only
interface references act as strong ones in the classic compiler, unless
they are not additionally marked as weak.
An object should never hold a strong reference to its owner, parent or any
other similar reference pointing back to its root.
The first object instance's reference count is 2 - there are two strong
references to it, FooBar and Foo . The second object instance's reference
count is 1 - there is only one strong reference pointing to it, Bar . The
whole object hierarchy is reachable through the root FooBar reference.
Now, if we set the FooBar reference to nil , or it goes out of scope, the
reference count of the first object will be decreased. Since there is a
strong reference in the second object pointing to the first one, the
reference count of the second object will remain higher than zero, and
the first object will not be destroyed. Even though both objects are no
longer reachable - the root reference no longer points to the first object,
they will both remain in memory until the application is terminated,
effectively creating a memory leak.
Another way of breaking that cycle would be assigning nil to the Bar
variable before assigning nil to the root FooBar .
Bar := nil;
FooBar := nil;
The above sequence will trigger the destruction of the second object,
since we have removed the last strong reference to it. During that
process, the Foo variable holding the second strong reference to the first
object will be cleared, leaving only a single strong reference - FooBar . Once
we assign nil to FooBar , the first object will be destroyed, too. Although that
works, it fits the definition of "clever" code, in that everyone who touches
the module must properly understand why and how it works.
12 Automatic Reference Counting in
Delphi
12.1 ARC in classic compiler - Interfaces
Before taking a deep dive into how reference counting in Delphi actually
works, knowing a bit of history can shed some light on how and why it is
implemented the way it is.
All COM interfaces must inherit from the base COM interface - IUnknown -
and subsequently all COM object implementations have to implement the
following three methods from IUnknown declaration:
While all three methods' implementations are required to satisfy the COM
interface model, only _AddRef and _Release are directly related to the
reference counting mechanism and depending on their implementation in
a class, reference counting on that particular class' object instances can
be either enabled, disabled, or delegated to another object instance.
When dealing with classes that implement interfaces, the compiler will
automatically insert calls to the reference counting methods _AddRef and
_Release at the appropriate places. This makes Delphi interface references
directly tied to memory management.
Regardless of the issues - that can and hopefully will be resolved as the
ARC compiler and related frameworks mature, the ARC compiler
constitutes a solid foundation for the future. The main problem is - and
will be in the foreseeable future - not the compiler itself, but code that has
to maintain compatibility with the classic compiler. Under such
circumstances, code has to be written following manual memory
management logic, which only drags the ARC side down and prevents us
from unleashing its full potential.
There are no magic solutions for porting existing code to the ARC
compiler. Depending on the code, porting can be anything from extremely
easy to extremely hard. There are no quick fixes, clever tricks or five-step
guides that can help.
This is not a simple change in syntax port where you have to do some
dull find-replace or name things differently changes and you are done.
This is a deep change in the memory management system, the very
foundation, and this is the kind of change that changes the basic rules of
the game. In order to play, you have to learn the new rules. There is no
easy way around that. And the new rules have been laid out in the ARC
chapters - and, in a more general form, throughout this whole book.
Only code that already used ARC in the classic compiler in the form of
reference counted classes through interface references, will run as-is in
the ARC compiler. Obviously, that code already follows the rules of the
reference counting mechanism.
The one thing that makes this port harder, is that there is no compiler
guidance, like there was, for instance, in Unicode conversion. However,
this is not due to any deficiency in the compiler, but due to the nature of
this change. Regardless of the size of your codebase and how good or
bad that code is, you will have to go through all the code, line by line,
making changes as you go, and sometimes you will have to make
multiple passes.
While there are different reference counting methods for interface and
object references, their purpose is the same - increasing and decreasing
the object instance's reference count. To avoid repetition while explaining
the algorithms I will use the xxxAddRef and xxxRelease notation when the same
explanation applies to both variants. Another way to refer to them will be
using the terms increasing and decreasing the reference count.
Specific aspects of the reference counting mechanism that are valid for
classic compilers are covered in the Interfaces chapter.
With the spotlight going to the strong (and weak) references, it is easy to
forget that the real actor is the object instance. The reference counting
happens there, and looking at it from the instance's perspective makes it
easier to understand the process. What really matters in the end, is not
how many visible or invisible references we have at any point in time, but
how many times the xxxAddRef and subsequently xxxRelease methods have
been called - and what is the actual value of the RefCount property of the
object. Having that in mind, it is also much easier to understand how we
can influence an object's reference count through weak references, or in
other words; how we can get a strong reference through a weak one.
There are two sides of reference counting that make up the whole. The
first is the instance itself, and the actual count stored within the instance,
and then there are references where changing the state (value) of the
reference triggers the reference counting mechanism in the associated
object instance.
While each object instance can have multiple references, weak and
strong, pointing to it at any point in time, each reference can only point to
a single object instance at any time - or be nil .
Besides explicit calls to reference counting methods that can be done
either within the object instance itself or through some outside reference,
all other triggers will happen through explicit or implicit strong references.
Knowing what code is responsible for reference state (value) changes
means knowing what kind of code triggers the reference counting
mechanism on the involved object instances and how. Each trigger works
independently of the others, and their impact is combined.
Explicit calls to the xxxAddRef and xxxRelease methods - either through any
kind of reference, weak or strong, that points to a non-nil valid object
instance, or within the instance itself - respectively increase or decrease
that instance's reference count.
Explicitly inducing reference counting falls into the Don't do it, unless you
really know what you are doing category. And by knowing what you are
doing, I mean you understand the principles behind the code and why at
that point you have to interfere with the reference counting mechanism.
"It fixes my code", is not a sufficient reason. Having said that, sometimes
you will have to write such code without really knowing why. Reference
counting is an extremely fragile mechanism if tampered with. You may fix
one piece of code, but break another as a result, and that may not be
immediately obvious, but only when you add more unrelated code into
the picture.
var
[weak] Intf: IFooInterface;
...
Intf._AddRef;
...
Intf._Release;
var
[unsafe] Intf: IFooInterface;
...
Intf._AddRef;
...
Intf._Release;
var
Intf: Pointer;
...
IInterface(Intf)._AddRef;
...
IInterface(Intf)._Release;
procedure TFooObject.Counting;
begin
__ObjAddRef;
...
__ObjRelease;
end;
procedure LocalObject;
var
Obj: TFooObject;
begin
// empty routine body
end;
and into
procedure LocalObject;
var
Obj: TFooObject;
begin
// initialization - part of the routine prologue
Obj := nil;
var
Obj: TFooObject;
...
Obj := nil;
and into
var
Obj: TFooObject;
...
// calls Obj.__ObjRelease if Obj is not nil at that point
_InstClear(TObject(Obj));
In the first scenario, assignment after construction, the value of the Source
is a pointer to a newly created object instance - at this point its reference
count is zero, and in the second scenario, the value of the Source is the
value of the reference we are assigning from.
In essence, the value of Dest is stored into a temporary pointer, the Source
reference count is incremented through xxxAddRef , the value of the Source
pointer is copied into Dest , and at last, the reference count of the original
Dest reference, now stored in a temporary pointer, is decreased through
xxxRelease .
Of course, just like all other reference counting helper methods, _IntfCopy
and _InstCopy have nil checks implemented all the way through, and if any
reference is nil at any point, calls to the relevant reference counting
methods are simply skipped.
var
Dest: TFooObject;
...
Dest := TFooObject.Create;
var
Dest: TFooObject;
...
_InstCopy(TObject(Dest), TFooObject.Create);
var
Source, Dest: IFooInterface;
...
Dest := Source;
var
Source, Dest: TFooObject;
...
Dest := Source;
Is translated to:
var
Source, Dest: IFooInterface;
...
_IntfCopy(IInterface(Dest), Source);
var
Source, Dest: TFooObject;
...
_InstCopy(TObject(Dest), Source);
will not cause any reference counting code to be injected into the
prologue and epilogue of the routine.
You can only pass [weak] or [unsafe] variables to out or var parameters
marked as [weak] or [unsafe] , respectively. Anything else will result in the
following compiler error:
procedure Local;
var
Foo: IFooInterface;
begin
Foo := NewFooInterface; // RefCount 1
end;
procedure Local;
var
Foo: TFooObject;
begin
Foo := NewFooObject; // RefCount 1
end;
The above examples with calling the function and assigning its result to a
local variable will not trigger the insertion of any additional reference
counting code, besides the code inserted due to the already described
mechanisms. The first is assigning to a strong reference Result after
object construction, and the second is inside the Local procedure, where
the local variable will be initialized before entering the scope and finalized
after exiting its scope.
procedure TFooContainer.IntfField;
begin
Intf := NewFooInterface; // RefCount 2
end;
procedure TFooContainer.ObjField;
begin
Obj := NewFooObject; // RefCount 2
end;
The above methods are equivalent to the following code, where each
assignment will then be translated to calls to helper methods according to
the previously explained rules.
procedure TFooContainer.IntfField;
var
Implicit: IFooInterface;
begin
// method prologue, initialize implicit variable
Implicit := nil;
procedure TFooContainer.ObjField;
var
Implicit: TFooObject;
begin
// method prologue, initialize implicit variable
Implicit := nil;
Basically, you just have to imitate what the compiler does behind the
scenes, but with explicitly declared variables. Since explicit variables are
under your control, you can explicitly nil them before the end of the
procedure call, at any point you need.
procedure TFooContainer.IntfField;
var
Explicit: IFooInterface;
begin
Explicit := NewFooInterface; // RefCount 1
Intf := Explicit; // RefCount 2
Explicit := nil; // RefCount 1
Actually, it is not about loops but about inline functions. However, loops
are a rather common place for hidden function calls. For instance,
accessing collection items in a loop. We don't usually think about it, but
collection items are backed by a getter function. If that function is
declared inline, then an implicit reference for storing the immediate result
of that function will be inserted inline with a smaller scope than that of the
procedure. If the getter function is not inlined, then the implicit reference
will have the same scope as the procedure where the code is located.
And that creates a loop effect where sometimes implicit references leak
and sometimes they do not.
type
IFoo = interface
['{DBBB81DA-1B1B-443C-B9DF-403C4A360E97}']
procedure Foo;
end;
procedure TFoo.Foo;
begin
Writeln('Foo');
end;
destructor TFoo.Destroy;
begin
Writeln('Foo destroyed');
inherited;
end;
procedure TestList;
var
i: integer;
List: IInterfaceList;
Intf: IFoo;
begin
List := TInterfaceList.Create;
Intf := TFoo.Create;
List.Add(Intf);
Intf := TFoo.Create;
List.Add(Intf);
Intf := TFoo.Create;
List.Add(Intf);
for i := 0 to List.Count-1 do
begin
Intf := List[i] as IFoo;
Intf.Foo;
end;
begin
TestList;
end.
The last Foo destroyed is written after end even though we have explicitly
cleared all references we possibly could and that is an indicator that there
is a hidden reference created behind the scenes to cater for the Intf :=
List[i] as IFoo assignment with an interface cast. However, replacing
TInterfaceList in the above example with TList<IInterface> will a generate
different, expected output.
Foo
Foo
Foo
Foo destroyed
Foo destroyed
Foo destroyed
end
Also, removing the cast to IFoo and changing the loop in the version using
TInterfaceList will not require implicit reference to cater for the cast.
Intf: IInterface;
...
for i := 0 to List.Count-1 do
begin
Intf := List[i];
end;
Foo destroyed
Foo destroyed
Foo destroyed
end
Intf: IInterface;
...
for i := 0 to List.Count-1 do
begin
Intf := List[i];
TFoo(Intf).Foo;
end;
We can even cast the interface to TFoo and call the Foo method without
triggering additional counting.
Intf: IInterface;
...
for i := 0 to List.Count-1 do
begin
Intf := List[i];
TFoo(Intf).Foo;
end;
Foo
Foo
Foo
Foo destroyed
Foo destroyed
Foo destroyed
end
TContainer = class(TObject)
strict private
FItem: IInterface;
function GetItem: IInterface;
public
procedure Init;
procedure Clear;
property Item: IInterface read GetItem;
end;
procedure TContainer.Init;
begin
FItem := TFoo.Create;
end;
procedure TContainer.Clear;
begin
FItem := nil;
end;
procedure TestContainer;
var
Container: TContainer;
Intf: IFoo;
begin
Container := TContainer.Create;
Container.Init;
Intf := nil;
Container.Clear;
Container.Free;
Writeln('end');
end;
begin
TestContainer;
end.
And the output will show we are dealing with an implicit reference in the
procedure scope:
Foo
end
Foo destroyed
Adding the inline directive to the GetItem function will also create an implicit
reference, but with a reduced scope.
function GetItem: IInterface; inline;
It has already been said that weak references of any kind can be used to
directly call reference counting methods, as well as assigning any kind of
weak reference to a strong one increments the reference count of that
particular instance.
There is one aspect of the [weak] reference tracking process you must be
aware of. Assigning to a [weak] reference immediately after construction,
before there are no other strong references to the object instance, will
instantly kill the object, regardless of whether you are using the classic
compiler and interfaces, or the ARC compiler and objects or interfaces.
begin
Ref := TInterfacedObject.Create;
Writeln(Assigned(Ref));
end.
=> FALSE
var
[weak] Ref: TObject;
begin
Ref := TObject.Create;
Writeln(Assigned(Ref));
end.
=> FALSE
14 Forming of a reference cycle
One of the simplest types of reference cycles is a parent-child
relationship.
TParent = class(TObject)
public
var Child: TChild;
constructor Create;
destructor Destroy; override;
end;
TChild = class(TObject)
public
var Parent: TParent;
constructor Create(AParent: TParent);
destructor Destroy; override;
end;
constructor TParent.Create;
begin
Writeln('Parent constructor');
inherited;
Child := TChild.Create(Self);
end;
destructor TParent.Destroy;
begin
Child.Free;
inherited;
Writeln('Parent destructor');
end;
destructor TChild.Destroy;
begin
inherited;
Writeln('Child destructor');
end;
procedure Test;
var
Root: TParent;
begin
Root := TParent.Create;
Root.Free;
end;
begin
Test;
end.
However, if we run that code under the ARC compiler, we will get the
following output showing that object instances were constructed, but
never released. We leaked two object instances, one TParent and one
TChild . Each time we call the Test procedure in our code, that procedure
will leak two object instances:
Parent constructor
Child constructor
IChild = interface
end;
constructor TParent.Create;
begin
Writeln('Parent constructor');
inherited;
Child := TChild.Create(Self);
end;
destructor TParent.Destroy;
begin
inherited;
Writeln('Parent destructor');
end;
destructor TChild.Destroy;
begin
inherited;
Writeln('Child destructor');
end;
procedure Test;
var
Root: IParent;
begin
Root := TParent.Create;
Root := nil;
end;
begin
Test;
end.
When we encounter such a cycle in our code, we can break the cycle by
making one of the strong references weak. The next logical question is,
which one of the offending references should be marked as weak?
While following ownership rules will give us clear picture about strong -
weak references in our object hierarchy, if we don't have a clear idea
about ownership there is an additional approach to solving this issue and
recognizing owning references. We can find owning references by simply
starting at root reference in some hierarchy and following all contained
references in a linear path. If any of the references points back to some
previous or any sibling reference in the hierarchy it will make a cycle, and
therefore such a reference should be weak.
Once we have determined which references are strong and which ones
are weak, we have to decide on a mechanism to use. There are three
ways to prevent reference counting on references: using the [weak] or
[unsafe] attributes, or storing the reference in a pointer.
By adding [weak] in the Parent field declaration from the above example we
solved the cycle:
TChild = class(TObject)
public
[weak] var Parent: TParent;
And running the corrected examples will give us the following output,
indicating all objects were successfully constructed and destructed:
Parent constructor
Child constructor
Parent destructor
Child destructor
TChild = class;
TChild = class(TObject)
public
[weak] var Parent: IParent;
constructor Create(const AParent: IParent);
destructor Destroy; override;
end;
constructor TParent.Create;
begin
Writeln('Parent constructor');
inherited;
Child := TChild.Create(Self);
end;
destructor TParent.Destroy;
begin
Child.Free;
inherited;
Writeln('Parent destructor');
end;
destructor TChild.Destroy;
begin
inherited;
Writeln('Child destructor');
end;
procedure Test;
var
Root: IParent;
begin
Root := TParent.Create;
Root := nil;
end;
begin
Test;
end.
Parent constructor
Child constructor
Child destructor
Parent destructor
In the above examples, the child's destructor runs before or after its
parent's, depending on the code in the destructor. Generally, the order of
their execution does not matter. However, if the child has to access its
parent during the destruction process for any reason, then the child must
be explicitly destroyed within the parent destructor, either by calling Free
or setting its reference to nil , depending on whether we are dealing with
object or interface references.
is part of the mechanism that bridges the gap between ARC and
Disposed
manual memory management.
If you look at the ARC side and its coding patterns - it certainly does not
recognize nor have the need for such a property. DisposeOf along with
Disposed has no purpose in pure ARC.
Disposed does not belong to ARC and it does not belong to manual
memory management. Any code using it, beyond the above mentioned
internal purpose, would be neither ARC compliant nor manual memory
management compliant. It would be bad code no matter how you look at
it. If some piece of code needs to check the Disposed property, that means
it is seriously broken by design and has to be fixed properly, by changing
the broken design.
15.4 Why does DisposeOf exist in Delphi ARC
compilers?
The TComponent class was designed with manual memory management in
mind, and this is true for all of its descendants. It uses a notification
mechanism that is not compatible with ARC memory management
because it relies on breaking strong reference cycles in the destructor.
Since TComponent is one of the core classes Delphi frameworks rely upon, it
must be able to function properly under ARC memory management,
while maintaining compatibility with the classic compiler.
The DisposeOf method enables the direct calling of object destructors, and
enables such legacy code to play along with ARC.
One thing must be noted here. Any code that uses or inherits from
TComponent or any other class that requires DisposeOf automatically becomes
legacy code in the context of proper ARC management, even if you write
it today. Basically, as long as the internal design of TComponent stays the
same, and as long as the core frameworks continue to use it, all code
that must interact with those frameworks will have to operate within the
constraints the TComponent notification system imposes within the ARC
environment.
Under non-ARC compilers, that mechanism ensures that you don't end
up with dangling pointers pointing to invalid - released - objects, and
under ARC compilers, clearing references to the destroying component
will decrease its reference count and break strong reference cycles.
So, for instance, if you have a TEdit and a TPopupMenu on your form and
assign that popup menu to the edit's PopupMenu property, the edit will hold a
strong reference to the popup menu in its FEditPopupMenu field, and the
popup menu will hold a strong reference to the edit in its FFreeNotifies list. If
you want to release either of those two components, you have to call
DisposeOf on them or they will just continue to exist.
While you can try to track those connections manually and break strong
reference cycles before you release any of those objects, that may not be
so easy to do in practice.
The following code will basically leak both components under ARC,
because they will hold strong references to each other, and after the
procedure is finished you will no longer have any external references that
point to either one of those components. However, if you replace Menu.Free
with Menu.DisposeOf you will trigger the Free Notification mechanism and break
the strong reference cycle.
procedure ComponentLeak;
var
Edit: TEdit;
Menu: TPopupMenu;
begin
Edit := TEdit.Create(nil);
Menu := TPopupMenu.Create(nil);
There are two rules that should be followed when releasing any TComponent
descended object under Delphi ARC compilers:
While in many cases the code will function properly even if the above
rules are not followed, such code would be rather fragile and could easily
be broken by other code introduced in seemingly unrelated places.
In other words, calling DisposeOf will not nil the calling reference, and as a
result, its reference count will not be decreased.
type
TFoo = class(TObject)
public
// Log is here for logging purposes only
class var Log: TStrings;
class constructor ClassCreate;
class destructor ClassDestroy;
public
destructor Destroy; override;
procedure FreeInstance; override;
end;
destructor TFoo.Destroy;
begin
Log.Add('Foo Destroy');
inherited;
end;
procedure TFoo.FreeInstance;
begin
Log.Add('Foo Free Instance');
inherited;
end;
procedure DoDispose;
var
Foo: TFoo;
begin
TFoo.Log.Add('');
Foo := TFoo.Create;
Foo.DisposeOf;
// Foo := nil;
TFoo.Log.Add('Foo DisposeOf finished');
TFoo.Log.Add('Foo Assigned ' + BoolToStr(Assigned(Foo), true));
end;
procedure DoFree;
var
Foo: TFoo;
begin
TFoo.Log.Add('');
Foo := TFoo.Create;
Foo.Free;
TFoo.Log.Add('Foo Free finished');
TFoo.Log.Add('Foo Assigned ' + BoolToStr(Assigned(Foo), true));
end;
procedure TestFoo;
begin
DoDispose;
DoFree;
end;
The above code will produce the following Log in the classic compiler.
Since the classic compiler leaves a dangling pointer after release, Assigned
will always produce True , unless we explicitly nil the reference. That is
expected output on classic compilers, where we don't expect nilling of the
reference.
Foo Destroy
Foo Free Instance
Foo DisposeOf finished
Foo Assigned True
Foo Destroy
Foo Free Instance
Foo Free finished
Foo Assigned True
Foo Destroy
Foo Free Instance
Foo Free finished
Foo Assigned False
As we can see, Free nils the calling reference on the ARC compiler, but
DisposeOf does not - after calling it Assigned(Foo) is True . If we explicitly nil the
Foo after calling DisposeOf , Assigned(Foo) will show False .
This is proof that DisposeOf does not nil the calling reference, but why
would we care? After all, neither Free nor DisposeOf don't nil the reference in
the classic compiler.
Well, in the classic compiler, Free and DisposeOf are not two different things
but one - Free . They are completely interchangable there. DisposeOf is here
to cater for the needs of particular code that requires executing the
destructor at a specific point even if there are other strong references to
the object instance. And DisposeOf does that, so what is the problem?
The problem is that in the classic compiler, calling Free (or DisposeOf ) will
completely destroy the object instance and release its memory. Unless
you are reusing the object reference, there is no need to reset it to nil .
The ARC side can completely release all instance memory only after
reference counting completes its job and when the reference count of the
object instance reaches zero. Not sooner. You can destroy all you want,
but the reference counting mechanism has to complete its job. And this is
the purpose of zombie objects. They are a thin shell kept alive just to give
the reference counting mechanism something it can work on.
In ARC, object instances can be fully released only after all strong
references to that instance are gone. How do you do that? Besides
simply waiting for them to go out of scope, you can also set those
references to nil to decrement the reference count and get your object
one step closer to the release point. So when you want to get rid of some
object so much that you actually call its destructor, you definitely would
not want to nil the reference you used in that process - because you are
a fan of The Walking Dead. Of course, you want that reference cleaned
up sooner rather than later. There is absolutely no reason in this
Universe, or any other, why DisposeOf should not nil the calling reference. If
the compiler is smart enough to treat Free on ARC as a nil assignment,
surely it could be smart enough to insert a nil assignment after you call
DisposeOf - it has all the information it needs to perform that operation.
In the above example code, our reference is eventually cleared and the
object instance fully released. Yes, that is true, but only because in that
simple example the object reference went out of scope almost
immediately.
Still, that does not deserve so much ranting, doesn't it? Well, you know
that thin shell I was talking about previously? It is not so thin after all.
Oh, and another small thingy. An additional side effect of not nilling the
reference is that any [weak] references are not cleared after the object is
disposed. They will also have to wait for completion of the reference
counting cycle.
And this is actually a rather severe issue. Instead of thin zombies running
around you can have nice fat ones, too. And while you can explicitly nil
the instance after calling DisposeOf or use some custom made DisposeAndNil
function, working around this one is anything but easy, bordering on the
impossible.
The issue here is that DisposeOf will only call the destructor chain, but the
CleanupInstance method responsible for cleaning up inner managed fields -
strings, objects, interfaces, dynamic arrays... - will be called only after the
reference count of the object instance reaches zero.
type
TFoo = class(TObject)
public
Text: string;
Data: array of byte;
Obj: TObject;
end;
var
Foo, FooRef: TFoo;
procedure DoSomething;
var
Log: string;
begin
Foo := TFoo.Create;
Foo.Text := 'Alive and kicking';
SetLength(Foo.Data, 1024);
Foo.Data[0] := 100;
Foo.Obj := TObject.Create;
FooRef := Foo;
Foo.DisposeOf;
Foo := nil;
Or you can use FinalizeRecord to clean them all up at once, but only if you
don't have any [weak] fields inside your class or its ancestors or
descendant classes, since this code will walk through the whole
inheritance chain.
destructor TFoo.Destroy;
var
ClassPtr: TClass;
InitTable: Pointer;
begin
ClassPtr := ClassType;
repeat
InitTable := PPointer(PByte(ClassPtr) + vmtInitTable)^;
if InitTable <> nil then
FinalizeRecord(Self, InitTable);
ClassPtr := ClassPtr.ClassParent;
until ClassPtr = nil;
inherited;
end;
If you want to confine FinalizeRecord to the current class you will have to
use the following code and explicitly write for that particular class:
destructor TFoo.Destroy;
begin
FinalizeRecord(Self, PPointer(PByte(TFoo) + vmtInitTable)^);
inherited;
end;
The combined effect of the above two issues can manifest itself in
different ways. From keeping more memory allocated than necessary, to
creating elusive bugs that are caused by an unexpectedly inaccurate
reference count of contained non-owned object and interface references.
Since DisposeOf does not decrease the reference count of the calling
reference, it is important to nil such references in destructors, otherwise
whole object hierarchies can stay alive much longer than needed and in
some cases even during the whole application lifetime.
The last, but not the least issue with DisposeOf is that it will break circular
references only if there is code in the destructor that explicitly resolves
them - like TComponent 's notification system does.
Reference cycles that are not handled by the destructor should be broken
using the [weak] or [unsafe] attributes on one of the references. That is also
the preferred ARC practice.
DisposeOf should not be used as a quick fix for breaking all reference cycles
(the ones it was never designed for) because it will not work and abusing
it can result in elusive memory leaks.
var
p: TParent;
begin
p := TParent.Create;
p.Child := TChild.Create(p);
p.DisposeOf;
p := nil;
end;
The above code will leak both child and parent object instances.
Combined with the fact that DisposeOf does not clear inner managed types
(including strings), such leaks can be huge depending on what kind of
data you are storing inside. The only (proper) way to break that cycle is
by changing TChild 's class declaration:
TChild = class(TObject)
public
[weak] var Parent: TParent;
constructor Create(AParent: TParent);
end;
16 Interfaces in the classic compiler
This is exactly what happens with interfaces and the fact that they are
entangled with memory management in the classic Delphi compiler. It is a
bit ironic that interfaces, as a tool for achieving better abstraction and
separation of concerns, actually have such a flaw.
Nevertheless, that does not mean they cannot be used for that purpose,
just that you have to think a bit more when using them. This is also more
of a problem for developers coming to Delphi from other languages,
where using interfaces does not involve special memory management
concerns. With reference counting disabled, there is almost no difference
between using non-interfaced object instances and interfaced ones. And
with reference counting enabled, you will get automatic memory
management, which allows you to do some neat things too.
If a class has reference counting disabled, you can use object references
to store such object instances and you can also safely pass them to
procedures that expect a particular interface supported by that class.
Since reference counting is disabled, such object instances have to be
released manually.
If further handling is done only through the code that does not trigger the
reference counting mechanism, that reference will behave like a regular
one. Unless we call Free on it, it will leak since there will be no automatic
cleanup inserted when this unsafe reference goes out of scope.
To be more precise, you must not mix the two if the underlying object
instance has reference counting enabled. Also, taking temporary object
references after we have initially properly constructed an object instance
and stored it into at least one strong interface reference is allowed.
Again, such an object reference must not be used after the last strong
interface reference is cleared, as it would represent a stale reference.
type
IFoo = interface
procedure Foo;
end;
procedure CanMix;
var
Foo: IFoo;
FooBar: TFooBar;
begin
// Reference counted class must be stored in interface reference
// after construction.
Foo := TFooBar.Create;
Foo.Foo;
If the object instance has reference counting disabled, we can safely mix
object and interface references. In other words, we can safely store
object instance in an object reference after construction and we can
safely use it where interface reference is required. The only thing we
have to keep in mind, is that we must make sure there are no interface
references with a lifetime longer than the lifetime of our object instance.
In other words, we must make sure that at the point where we call Free on
such an object instance, there are no active interface references that will
call _IntfClear when they go out of scope on an already dead object
instance - resulting in an access violation exception.
One good example that shows why mixing object and interface
references is not the smartest idea ever, is the fact that some
System.SysUtils.Supports function overloads will kill the reference
counted object instance if the instance has a broken reference counting
mechanism. So merely testing whether the instance supports a particular
interface can end in disaster.
destructor TFoo.Destroy;
begin
Writeln('Foo is gone');
inherited;
end;
var
Foo: TFoo;
begin
Foo := TFoo.Create;
if Supports(Foo, IInterface) then
begin
end;
Writeln('Foo must be alive here');
// Foo.Free; // this would crash
end.
Running the above code will produce the following output, showing that
Foo is gone before its time. Furthermore, if we try to call Foo.Free after
calling Supports we will have an access violation exception because we will
try to release an already released object.
Foo is gone
Foo must be alive here
After making the above change we would get the expected output.
Foo must be alive here
Foo is gone
type
TNonManaged = class(TObject, IInterface)
protected
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
And then you can freely use such a class and its descendants through
object references. Of course, since automatic management is disabled,
you will have to explicitly release such object instances.
var
Obj: TNonManaged;
begin
Obj := TNonManaged.Create;
try
...
finally
Obj.Free;
end;
end;
Since each class implements its own reference counting methods, this
gives you the opportunity to write reference counting methods in a
manner that will enable or disable counting depending on some
constructor parameters or other conditions. Reference counting methods
will still be inserted according to the rules and called, but if disabled, the
object instances will not be automatically released and will have to be
handled manually just like regular objects.
The only important thing to remember when using such a class is that if
reference counting is enabled, it must be stored within a strong interface
reference, and should not be manually released.
var
Obj: TManagedObject;
begin
// Reference counting is disabled
Obj := TManagedObject.Create;
try
...
finally
Obj.Free;
end;
end;
var
Intf: IInterface;
begin
// Reference counting is enabled
// will be released when last strong reference goes out of scope
Intf := TManagedObject.Create(true);
...
end;
var
Doc: IXMLDocument;
begin
Doc := TXMLDocument.Create(nil);
// or
Doc := TXMLDocument.Create(FileName);
...
// will be automatically released when Doc goes out of scope
end;
Unfortunately, this issue is not confined only to the classic compiler. The
ARC compiler also suffers from it, just with different, equally fatal
manifestations - depending on the particular compiler used.
begin
ReportMemoryLeaksOnShutdown := true;
LeakTest(TInterfacedObject.Create);
end;
The above code will leak the TInterfacedObject instance constructed inline in
the procedure call. Removing the const modifier from the procedure
declaration will remove the leak.
procedure LeakTest(Intf: IInterface);
Don't ever use such a fix. It will just delay your problems, and not fix the
real cause.
So what is the problem here and how to properly solve it?
Let's start with the fix. It is fairly simple, don't construct an object inside
the procedure call, use an additional variable to construct the object first
and then pass it to the procedure.
procedure LeakTest(const Intf: IInterface);
begin
end;
var
Intf: IInterface;
begin
ReportMemoryLeaksOnShutdown := true;
Intf := TInterfacedObject.Create;
LeakTest(Intf);
end;
The effect you are seeing is the same you would have if you initially
stored your object instance in an object reference. The following example
leaks just the same if passed as a const parameter.
var
Obj: TInterfacedObject;
begin
ReportMemoryLeaksOnShutdown := true;
Obj := TInterfacedObject.Create;
LeakTest(Obj);
end;
Why is that a defect in the compiler? You clearly don't want reference
counting triggered in that procedure and that is exactly what is happening
here.
Well, there is another, less painful, solution to the problem when you are
dealing with interfaces. If you explicitly cast your object as an interface,
the compiler will add a hidden implicit reference and properly initialize
reference counting of that instance.
LeakTest(TInterfacedObject.Create as IInterface);
However, this still does not make this issue any less serious. First, you
have to know that this issue exists in order to use the proper solution.
That means every Delphi developer will experience this issue at some
point in time, get burned, waste more or less time and eventually learn
the hard way. Next, explicitly casting the object as interface does not
constitute a common coding pattern in Delphi. Unless you want some
other developer to jump in and correct your silly code without knowing
that you are working around a bug in the compiler, you will have to add a
comment explaining why this piece of code is written the way it is.
And even when you are aware of this issue, it is still very possible that, in
a moment of distraction, you will forget about it and write broken code.
Another issue with this kind of code is that you may be tempted to write it
only if the parameter is declared as const , and avoid duplicate counting
when dealing with value parameters. But const parameters are merely an
optimization feature when it comes to reference counted instances, and
they are subject to change. As result, your code can stop working
properly if someone somewhere tweaks some procedure declaration.
Since this issue has many variations, using the proper solutions in
various places can lead to fragile code that can easily be broken. Instead
of keeping track of parameters and their declarations, there is one simple
rule you can use instead. Don't construct objects inline and always use
an additional variable. This is easy to remember and easy to follow.
A more elaborate example that exhibits not only leaks, but also crashes,
consists of a default Multi Device Application, with four buttons and a
memo. Add the following code and observe the effects. After each button
click, properly working code will add a Foo destroyed line to the memo.
Hint, the UseConstReference procedure will result in a crash.
TFoo = class(TInterfacedObject)
public
destructor Destroy; override;
end;
destructor TFoo.Destroy;
begin
Form1.Memo1.Lines.Add('Foo destroyed');
inherited;
end;
This is where managed types come in. They are always initialized no
matter where they are stored and we can use them as a properly
initialized flag inside the records. Since strings are managed types they
can be used for implementing such flags.
type
TFlagable = record
private
FFlag: string;
function GetIsSet: boolean;
procedure SetIsSet(Value: boolean);
public
property IsSet: boolean read GetIsSet write SetIsSet;
end;
var
Flagable: TFlagable;
begin
Writeln(Flagable.IsSet);
end;
If you need more than two states, you can appropriately set different
values into the FFlag string.
However, if the flag only has two states, an interface can be used instead
of a string.
type
TFlagable = record
private
FFlag: IInterface;
function GetIsSet: boolean;
procedure SetIsSet(Value: boolean);
public
property IsSet: boolean read GetIsSet write SetIsSet;
end;
The above implementation will work just as the string one, but creating an
object on the heap is a slightly wasteful approach. Fortunately, to
implement an interface all you need are three methods and a virtual
method table. Since in this case you don't need any actual reference
counting to take place, this functionality can be easily achieved by some
plain functions and array that will simulate an interface VMT. The first
parameter in those functions serves as the implicit Self parameter that all
methods have.
function NopAddRef(inst: Pointer): integer; stdcall;
begin
Result := -1;
end;
const
FakeInterfaceVTable: array [0 .. 2] of Pointer =
(@NopQueryInterface, @NopAddRef, @NopRelease);
FakeInterfaceInstance: Pointer = @FakeInterfaceVTable;
16.6.1 System.Classes.TInterfaceList
Intf := TInterfacedObject.Create;
List.Add(Intf);
Intf := TInterfacedObject.Create;
List.Add(Intf);
Writeln(List.Count);
end.
IBar = interface
['{180296D2-CCB8-4033-9F47-4D70434A9721}']
procedure Bar;
end;
procedure TFooBar.Bar;
begin
Writeln('Bar');
end;
procedure TFooBar.Foo;
begin
Writeln('Foo');
end;
var
List: IInterfaceList;
Intf: IInterface;
Foo: IFoo;
begin
List := TInterfaceList.Create;
Intf := TFooBar.Create;
List.Add(Intf);
Intf := TFooBar.Create;
List.Add(Intf);
Intf := TFooBar.Create;
List.Add(Intf);
Intf := List[0];
(Intf as IFoo).Foo;
(Intf as IBar).Bar;
if Supports(Intf, IFoo, Foo) then Foo.Foo;
end.
The above code will output:
Foo
Bar
Foo
16.6.2 System.Generics.Collections.TList<T>
For easier access to stored references that does not need any interface
querying, a generic TList<T> can be used instead of TInterfaceList . Since
TList<T> is not a reference counted class, we have to manually release the
list when we are done.
var
List: TList<IFoo>;
Intf, Foo: IFoo;
begin
ReportMemoryLeaksOnShutdown := true;
List := TList<IFoo>.Create;
try
Intf := TFooBar.Create;
List.Add(Intf);
Intf := TFooBar.Create;
List.Add(Intf);
Intf := TFooBar.Create;
List.Add(Intf);
Foo := List[0];
Foo.Foo;
finally
List.Free;
end;
end.
17 Anonymous methods - a trouble
without a name
Delphi has two procedural types capable of storing and executing plain
functions and procedures, as well as methods - functions and procedures
declared within the context of a class.
Anonymous methods add a third one. As their name suggests, they are
methods that do not have an associated identifier - they have no name.
They represent blocks of code that can be assigned to a variable or
passed as a parameter and executed later on, in the same manner as
other procedural types. Anonymous methods are similar to closures in
other languages, as they implement variable capture, which enables the
usage of variables from the context in which they are defined.
IAnonymousFunc = interface
function Invoke: Integer;
end;
IAnonymousProcWithParameters = interface
procedure Invoke(x, y: Integer);
end;
IAnonymousFuncWithParameters = interface
function Invoke(x, y: Integer): Integer;
end;
Variable capture solved, and they all lived happily ever after.
Well no, not really. It all works well, until you start capturing reference
counted object instances exposed through interfaces in the classic
compiler, or all instances in the ARC compiler. Actually, you can create
nice cycles with records, too.
Delphi also suffers from some issues that make the problems related to
anonymous method cycles more prominent:
Execute(procedure
begin
Writeln('Anything ', Number);
end);
Number := 0;
Execute(procedure
begin
Writeln('Bang ', Number);
Number := 42;
end);
Execute(procedure
begin
Writeln('Resurrected ', Number);
end);
end;
begin
Test;
end.
Anything 42
Bang 0
Resurrected 42
Not only can we capture a variable, but we can change its value from
within the anonymous methods.
Let's change the Test method, and instead of executing methods inline,
store them into variables for later execution:
procedure Test;
var
Number: Integer;
Proc1, Proc2, Proc3: TAnonymousProc;
begin
Number := 42;
Proc1 := procedure
begin
Writeln('Anything ', Number);
end;
Number := 0;
Proc2 := procedure
begin
Writeln('Bang ', Number);
Number := 42;
end;
Proc3 := procedure
begin
Writeln('Resurrected ', Number);
end;
Execute(Proc1);
Execute(Proc2);
Execute(Proc3);
end;
Anything 0
Bang 0
Resurrected 42
Now what? Why is Anything 0 printed? Well, obviously, for the same reason
that we were able to change values from within the anonymous methods
- because we captured the location of the variable and not its value.
Before we called any of our anonymous methods, we executed the
following code:
Number := 42;
Number := 0;
To move things further, let's declare Proc1 , Proc2 and Proc3 outside the Test
procedure and call them outside its scope.
var
Proc1, Proc2, Proc3: TAnonymousProc;
procedure Test;
var
Number: Integer;
begin
Number := 42;
Proc1 := procedure
begin
Writeln('Anything ', Number);
end;
Number := 0;
Proc2 := procedure
begin
Writeln('Bang ', Number);
Number := 42;
end;
Proc3 := procedure
begin
Writeln('Resurrected ', Number);
end;
end;
begin
Test;
Execute(Proc1);
Execute(Proc2);
Execute(Proc3);
end.
Anything 0
Bang 0
Resurrected 42
What exactly are we testing in the Test procedure? First, we will acquire
an anonymous method by using the Composer function. Then, we will
execute that function two times to observe what is happening with the
captured variables, and then we will acquire the anonymous method
again and execute it once - merely to prove that each time we call the
anonymous method factory, we will be served with a brand new
anonymous method, which captured a fresh set of local variables. This is
expected behavior, because every time you call a function, a new stack
frame will be created to serve that call, and local variables will be
allocated on that stack frame.
function Composer: TAnonymousStringFunc;
var
Text: string;
Number: Integer;
begin
Text := 'Number';
Number := 5;
Result := function: string
begin
Inc(Number);
Result := Text + ' ' + IntToStr(Number);
end;
end;
procedure Test;
var
Ref1, Ref2: TAnonymousStringFunc;
Str: string;
begin
// Get anonymous method
Ref1 := Composer();
// Call anonymous method
Str := Ref1;
Writeln(Str);
// Call anonymous method
Str := Ref1;
Writeln(Str);
begin
ReportMemoryLeaksOnShutdown := true;
Test;
end.
Number 6
Number 7
Number 6
Number 8
Records are value types, so capturing records should work just about the
same way:
TContentRec = record
Text: string;
Number: Integer;
constructor Create(const AText: string; ANumber: Integer);
end;
Calling Test again shows the same output. Good. But we leaked two
TContentObject instances. Not so good.
Actually, the above code leaks in the classic compiler, but not in ARC,
where all object instances are reference counted and their memory is
automatically managed.
Number 6
Number 7
Number 6
Number 8
6
2
6
2
If you are still not convinced we are dealing with a dangling pointer, just
nil the Content variable and enjoy the access violation exception that
causes.
...
Content.Free;
Content := nil;
So, this approach obviously does not work. What if we Free the Content
from within the body of the anonymous method? By now, it should be
pretty obvious that that approach will not work either, but...
function Composer: TAnonymousStringFunc;
var
Content: TContentObject;
begin
Content := TContentObject.Create('Number', 5);
Result := function: string
begin
Inc(Content.Number);
Result := Content.Text + ' ' + IntToStr(Content.Number);
Content.Free;
Content := nil;
end;
end;
Ref2 := Composer();
Str := Ref2;
Writeln(Str);
end;
Of course, if you want to have the same output as before, 6 7 6 8 , you will
not like this one:
Number 6
Number 6
Number 6
Number 6
If you need to have a local object instance for whatever reason, and you
also need to update its fields, you will have to add additional local
variables (value types) that can handle being captured by the anonymous
methods.
function Composer: TAnonymousStringFunc;
var
Number: Integer;
begin
Number := 5;
Result := function: string
var
Content: TContentObject;
begin
Inc(Number);
Content := TContentObject.Create('Number', Number);
Result := Content.Text + ' ' + IntToStr(Content.Number);
Content.Free;
end;
end;
The only thing we have to keep in mind here is that a locally constructed
object instance will be kept alive as long as the anonymous method
referencing it is alive. In our example, that will happen in the epilogue of
the Test procedure. If we needed to release it sooner, we would have to
set the Refxxx variable holding that particular instance to nil .
If you think that was just a joke... well, it is a joke, but only to a point.
Capturing variables is a powerful feature, but also one that can easily be
abused, leading to bad code, regardless of cycles. So when you use
anonymous methods and their ability to capture variables from the
surrounding context, make sure that you are not making a real mess of
your code along the way.
Probably the most devious reference cycles are ones created with
several anonymous methods. Other kinds of reference cycles involving
anonymous methods are more obvious and are more easily recognized,
as they resemble reference cycles created by regular strong references
to object instances. You just have to remember that any anonymous
method reference is an interface reference in disguise.
Proc2 := procedure
begin
Proc1;
Writeln('Procedure 2');
end;
Proc2;
end;
begin
ReportMemoryLeaksOnShutdown := true;
Test;
end.
The above code will leak the hidden object instance used to back up
those two anonymous methods. This leak happens because of specific
implementation details - in this case, the fact that both of our methods are
backed up by the same hidden object instance. Since the second method
references the first one, it effectively references its own supporting
instance.
Since Proc1 is the one causing the cycle, we can solve this leak by
explicitly setting it to nil before the end of the local procedure.
...
Proc2;
Proc1 := nil;
end;
Even if we removed the explicit Proc2 variable, we would still create a leak,
because the parameter passed to the Execute procedure would be
implicitly captured within the same context.
procedure Test;
var
Proc1: TAnonymousProc;
begin
Proc1 := procedure
begin
Writeln('Procedure 1');
end;
Execute(procedure
begin
Proc1;
Writeln('Procedure 2');
end);
end;
However, if we move the first procedure to the second one, they will be
defined in different code blocks and supported by two different instances.
procedure Test;
begin
Execute(procedure
begin
Execute(procedure
begin
Writeln('Procedure 1');
end);
Writeln('Procedure 2');
end);
end;
procedure Test;
var
Rec: TAnonymousRec;
begin
Rec.Number := 5;
Rec.Proc := procedure
begin
Writeln(Rec.Number);
end;
Rec.Proc();
end;
begin
ReportMemoryLeaksOnShutdown := true;
Test;
end.
Since records are value types, the above code will create a cycle
because the whole record value (holding a reference to the anonymous
method) will be captured. Converting TAnonymousRec to a class will behave
differently in the classic and ARC compilers, because the reference
counting mechanism would create a strong reference cycle between the
object instance and the anonymous method that captured that instance.
type
TAnonymousObject = class(TObject)
public
Number: Integer;
Proc: TAnonymousProc;
end;
Obviously, Proc is the culprit here and should be nilled to break the cycle.
But there is one thing we can exploit here. The ARC version leaks in the
ARC compiler, but if we use the classic compiler version, it does not leak
in the ARC compiler. The secret is in the Obj.Free call, which translates to
Obj := nil in the ARC compiler, effectively breaking the cycle. Of course,
from an ownership point of view, it is the wrong way to break the cycle,
but since the classic compiler code works well in both compilers, thus
making such code cross-platform, it is acceptable.
Using a reference counted class through an interface reference would
also create the same kind of leak, that can also be resolved by setting the
reference to nil before the end of the local procedure. However, while
calling Free on an object instance would not raise any eyebrows, nilling an
interface reference just before it goes out of scope might. It would be
prudent to comment such code to prevent someone from clearing it out
by mistake, as it might seem like redundant cruft.
If you cannot wrap your head around why setting an object or interface
reference to nil immediately before it goes out of scope can clear the
above cycle, the secret is in the implicit code added by the compiler in
the epilogue - another call to _InstClear or _IntfClear to finalize the local
variable as covered in the Strong reference initialization and finalization
chapter. Because of this, the reference count of the Obj instance will be
decreased twice, and that is what breaks the cycle in this code.
The previously presented code not only has obvious flaws, but it is also
quite pointless. Its purpose is not to be a pattern you would use, but to be
a learning model.
In such cases you have to either use reference counted object instances,
or you must release the object from within the method; but in the latter
case you must call such an anonymous method only once.
Such an asynchronous scenario can be created with TTask from the RTL
Parallel Programming Library.
type
TProcessor = class(TObject)
protected
FData: TData;
...
public
procedure Run;
end;
procedure TProcessor.Run;
var
Task: ITask;
begin
FData.Prepare;
Task := TTask.Create(procedure
begin
FData.Process;
FData.Processed := true;
end);
Task.Start;
end;
The above Run method will not create any strong cycles even though we
are accessing TProcessor fields inside an anonymous method, because Task
is a local variable. However, if for any reason we have to make that Task
variable accessible to a broader context, even temporarily, we will have a
strong reference cycle.
procedure TProcessor.Run;
begin
FData.Prepare;
FTask := TTask.Create(procedure
begin
FData.Process;
FData.Processed := true;
FTask := nil;
end);
FTask.Start;
end;
If the above requirement cannot be satisfied, you will have to break the
cycle by some other method: separating dependencies, limiting the scope
of anonymous methods if you have more of them, using different
transient variables for storing values that must be captured...
If you can break a cycle by weak references, great; you just have to use
standard methods for making weak references and you are done...
Not so fast.
You can use [unsafe] or you can use a pointer, but you cannot use the
[weak] attribute because the compiler likes them strong.
QP report RSP-19204
program RSP_19204;
{$APPTYPE CONSOLE}
uses
System.SysUtils;
type
TAnonymousProc = reference to procedure;
ICounted = interface
function GetRefCount: Integer;
end;
procedure TestUnsafeProc;
var
Intf: ICounted;
[unsafe] u: ICounted;
Proc: TAnonymousProc;
begin
Intf := TCounted.Create;
u := Intf;
Proc := procedure
begin
Writeln('Unsafe proc ', u.GetRefCount);
end;
Proc();
end;
procedure TestWeakProc;
var
Intf: ICounted;
[weak] w: ICounted;
Proc: TAnonymousProc;
begin
Intf := TCounted.Create;
Proc := procedure
begin
Writeln('Weak proc ', w.GetRefCount);
end;
Proc();
end;
procedure TestWeak;
var
Intf: ICounted;
[weak] w: ICounted;
begin
Intf := TCounted.Create;
w := Intf;
Writeln('Weak ', w.GetRefCount);
end;
begin
TestWeak;
TestUnsafeProc;
TestWeakProc;
end.
So, what happened here? References marked with [weak] attribute must
not contribute to reference counting. They should merely be tracked and
zeroed when the object instance is destroyed. However, that is not what
happens here. The reference count in all three test cases should be one,
but it is not when the weak reference is touched by an anonymous
method.
RSP_19204.dpr.43: w := Intf;
0041B769 8B45F4 mov eax,[ebp-$0c]
0041B76C 83C00C add eax,$0c
0041B76F 8B55FC mov edx,[ebp-$04]
0041B772 E8FDF3FEFF call @IntfCopy
w := Intf;
Proc := procedure
begin
Writeln('Weak proc ', Intf.GetRefCount);
end;
will result in the following code, where _IntfWeakCopy is used for assigning:
RSP_19204.dpr.43: w := Intf;
0041B6CC 8D45FC lea eax,[ebp-$04]
0041B6CF 8B55F4 mov edx,[ebp-$0c]
0041B6D2 8B520C mov edx,[edx+$0c]
0041B6D5 E8DA05FFFF call @IntfWeakCopy
Since the compiler breaks the [weak] attribute for references captured by
anonymous methods, you cannot use it for breaking strong reference
cycles. What makes this rather unfortunate is that if you are not sure that
the lifetime of the captured object instance will be longer than the lifetime
of the anonymous method, you cannot use [unsafe] or pointers either, or
you can end up having mysterious crashes or bugs when you access a
dangling pointer.
While the previous example does not actually create any leaks because it
does not have any strong reference cycles, it is rather easy to create one:
type
TLeaked = class(TInterfacedObject)
public
Proc: TAnonymousProc;
end;
procedure Test;
var
Intf: IInterface;
[weak] w: IInterface;
begin
Intf := TLeaked.Create;
w := Intf;
TLeaked(Intf).Proc := procedure
begin
if Assigned(w) then
end;
end;
begin
ReportMemoryLeaksOnShutdown := true;
Test;
end.
However, there is still something that works in such scenarios: Using any
kind of weak wrapper, like the one mentioned in the Storing weak and
unsafe references in collections chapter, or the one in the Coding
patterns - Weak chapter.
w: IWeak<IInterface>;
begin
Intf := TLeaked.Create;
w := TWeak<IInterface>.Create(Intf);
While the above issue is presented through interface references and can
be reproduced in the classic compiler, the same issue can be observed in
the ARC compiler using either interface or object references.
Delphi Berlin 10.1 introduced support for the [weak] and [unsafe] attributes
in the classic compiler. This improved the handling of [unsafe] interface
references. Using pointers as a workaround is no longer necessary, and
having auto-zeroed [weak] interface references opened up new
possibilities in handling complex scenarios with reference counted
objects.
type
TWeakIntfObj<T: IInterface> = class(TObject)
public
[weak] Value: T;
constructor Create(const AValue: T);
end;
Obviously, records are the clear winner here. Also, using records does
not involve additional heap allocations, reference counting, or taking care
of releasing the wrapper itself.
Our implementing class has two constructors. The first one is for
convenience, and automatically creates the wrapped object instance
using a default constructor. If the wrapped class requires calling a custom
constructor, we can also wrap an existing object instance. We will release
the wrapped object in the destructor, achieving our goal of automatic
destruction of the wrapped object when the reference counted wrapper
instance goes out of scope.
interface
type
ISmartPointer<T> = interface
function Value: T;
end;
implementation
constructor TSmartPointer<T>.Create;
begin
inherited Create;
FValue := T.Create;
end;
destructor TSmartPointer<T>.Destroy;
begin
FValue.Free;
inherited;
end;
function TSmartPointer<T>.Value: T;
begin
Result := FValue;
end;
end.
Now we can wrap any class we want, TStringList for instance, and use it
the in following manner:
procedure Managed;
var
sl: ISmartPointer<TStringList>;
begin
sl := TSmartPointer<TStringList>.Create();
sl.Value.Add('I am inside automanaged StringList');
end;
procedure Managed;
var
sl: ISmartPointer<TStringList>;
begin
sl := TSmartPointer<TStringList>.Create();
sl.Add('I am inside an automanaged StringList');
UseStrings(sl);
end;
procedure Managed;
var
sl: TSmartPointer<TStringList>;
begin
sl := TSmartPointer<TStringList>.Create();
sl.Value.Add('I am inside an automanaged StringList');
end;
Nice. We got rid of the try...finally blocks, we cannot accidentally leak
memory, the code is shorter and more readable, but on the other hand
we have to use Value whenever we want to access our wrapped object
instance. It hurts readability, and refactoring code from the unmanaged to
the managed variant will require more extensive changes.
interface
type
ISmartPointer<T> = reference to function: T;
implementation
constructor TSmartPointer<T>.Create;
begin
inherited Create;
FValue := T.Create;
end;
function TSmartPointer<T>.Invoke: T;
begin
Result := FValue;
end;
function TSmartPointer<T>.Extract: T;
begin
Result := FValue;
FValue := nil;
end;
end.
Now we can use our Smart Pointer and access a wrapped instance just
like we would use a regular object instance. The only differences are in
the instance's declaration and creation call - quite an acceptable trade-
off.
procedure Smart;
var
sl: ISmartPointer<TStringList>;
begin
sl := TSmartPointer<TStringList>.Create();
sl.Add('I am inside automanaged StringList');
end;
So when you write sl.Add in the above code, behind the curtains that
translates to a sl.Invoke function call which returns a TStringList instance
from FValue , on which we can use the Add method.
The above implementation has the Extract function that is used for
disassociating the wrapped object from the Smart Pointer without
releasing it. It is used for ownership transfer.
17.7 Lazy
Lazy initialization is a common pattern for constructing heavyweight
object instances on demand. Some languages even have such
capabilities built in, and instances are constructed when they are
accessed for the first time during program execution. There is another
pattern often related to lazy initialization, and that is to release such an
object when it is not actively being used at the moment, until the next
time it will be required.
As we all know, a Death Star is a huge thing. When you are operating
your Imperial Fleet, the last thing you need is something the size of a
small moon, just sitting in your memory. It would be great if you could
construct the Death Star on demand, only when you actually want to blow
up a planet and not sooner. On the other hand, TIE fighters are much
smaller and do not have to be lazy instantiated. They are here to show
the difference in behavior between a regular owned field and a lazy one.
While avoiding accessing nil instances in the TImperialFleet class would not
pose too much of a problem at this point because there is not a lot of
functionality there, as it grows and more code is added, dealing with nil
could become problematic. Using a lazy wrapper is easy to implement
and can save you much grief.
program LazyDeathStar;
{$APPTYPE CONSOLE}
type
TTIEFighter = class(TObject)
public
procedure Fight;
constructor Create;
destructor Destroy; override;
end;
TDeathStar = class(TObject)
public
constructor Create;
destructor Destroy; override;
procedure DestroyPlanet(const Planet: string);
end;
TDeathStarLazyWrapper = class(TObject)
strict private
FInstance: TDeathStar;
function GetIsAssigned: boolean;
function GetInstance: TDeathStar;
public
destructor Destroy; override;
property IsAssigned: boolean read GetIsAssigned;
property Instance: TDeathStar read GetInstance;
end;
TImperialFleet = class(TObject)
strict protected
TIE: TTIEFighter;
DeathStar: TDeathStarLazyWrapper;
public
constructor Create;
destructor Destroy; override;
procedure Fight;
procedure DestroyPlanet(const Planet: string);
end;
constructor TTIEFighter.Create;
begin
inherited;
Writeln('TIE ready');
end;
destructor TTIEFighter.Destroy;
begin
Writeln('TIE destroyed');
inherited;
end;
procedure TTIEFighter.Fight;
begin
Writeln('TIE engaged');
end;
constructor TDeathStar.Create;
begin
inherited;
Writeln('Death Star constructed');
end;
destructor TDeathStar.Destroy;
begin
Writeln('Death Star destroyed');
inherited;
end;
destructor TDeathStarLazyWrapper.Destroy;
begin
FInstance.Free;
inherited;
end;
constructor TImperialFleet.Create;
begin
inherited;
TIE := TTIEFighter.Create;
DeathStar := TDeathStarLazyWrapper.Create;
end;
destructor TImperialFleet.Destroy;
begin
Writeln('Destroying Imperial Fleet');
if DeathStar.IsAssigned then Writeln('Death Star is operational');
TIE.Free;
DeathStar.Free;
inherited;
end;
procedure TImperialFleet.Fight;
begin
TIE.Fight;
end;
var
Fleet: TImperialFleet;
begin
Fleet := TImperialFleet.Create;
try
Fleet.Fight;
Fleet.Fight;
Fleet.DestroyPlanet('Alderaan');
Fleet.Fight;
finally
Fleet.Free;
end;
end.
TIE ready
TIE engaged
TIE engaged
Death Star constructed
Alderaan has been destroyed!!!
TIE engaged
Destroying Imperial Fleet
Death Star is operational
TIE destroyed
Death Star destroyed
The above example still has one drawback. As soon as you need to blow
up a planet, you will get stuck with that thing the size of a small moon in
memory. How about releasing it if you won't need it in the foreseeable
future?
TImperialFleet = class(TObject)
...
procedure DestroyPlanet(const Planet: string);
procedure PackDeathStar;
end;
procedure TDeathStarLazyWrapper.Clear;
begin
FInstance.Free;
FInstance := nil;
end;
procedure TImperialFleet.PackDeathStar;
begin
DeathStar.Clear;
end;
or if you prefer FreeAndNil , now is a good place to use it.
procedure TDeathStarLazyWrapper.Clear;
begin
FreeAndNil(FInstance);
end;
And now the Imperial Fleet can rule the galaxy without worrying it will run
out of space...
var
Fleet: TImperialFleet;
begin
Fleet := TImperialFleet.Create;
try
Fleet.Fight;
Fleet.Fight;
Fleet.DestroyPlanet('Despayre');
Fleet.DestroyPlanet('Alderaan');
Fleet.PackDeathStar;
Fleet.Fight;
Fleet.DestroyPlanet('Yavin IV');
finally
Fleet.Free;
end;
TIE ready
TIE engaged
TIE engaged
Death Star constructed
Despayre has been destroyed!!!
Alderaan has been destroyed!!!
Death Star destroyed
TIE engaged
Death Star constructed
Yavin IV has been destroyed!!!
Destroying Imperial Fleet
Death Star is operational
TIE destroyed
Death Star destroyed
17.8 Weak
Zeroing weak references greatly simplify coding when you need to hold a
reference to some collaborator object but you are not its owner. However,
they are only available in the ARC compiler, and the classic compiler
supports such weak references - declared with the [weak] attribute - only
for interface references, and even then only since Delphi Berlin 10.1.
But that does not mean weak references are out of reach. Delphi
provides all that is needed for a weak reference container implementation
that will do all the hard work and increase code safety when dealing with
non-owned references. Such a container can also be used for storing
weak references in collections.
The IWeak interface and the WeakReferences class should not be used directly,
as they only provide necessary supporting functionality.
When the original object gets destroyed, the registered interceptor will
invoke the WeakReferences.RemoveInstance procedure, which will zero out all
existing weak references that point to that object.
unit ZWeakU;
interface
uses
System.TypInfo,
System.Rtti,
System.Classes,
System.Generics.Collections;
type
// support declarations
PInterface = ^IInterface;
IWeak = interface
procedure Zero;
end;
WeakReferences = class
strict private
class var Interceptors: TObjectDictionary<TClass, TVirtualMethodInterceptor
class var Instances: TObjectDictionary<Pointer, TList<IWeak>>;
class constructor ClassCreate;
class destructor ClassDestroy;
public
class procedure AddInterceptor(const Instance: TObject); static;
class procedure RemoveInstance(const Instance: TObject); static;
class procedure AddReference(const Ref: IWeak; const Instance: TObject);
static;
class procedure RemoveReference(const Ref: IWeak; const Instance: Pointer);
static;
end;
IWeak<T> = interface
procedure Clear;
function GetRef: T;
property Ref: T read GetRef;
end;
implementation
Interceptor := TVirtualMethodInterceptor.Create(Instance.ClassType);
Interceptor.OnBefore := procedure(
Intercepted: TObject; Method: TRttiMethod; const Args: TArray<TValue>
out DoInvoke: boolean; out Result: TValue)
begin
if Method = FreeMethod then
begin
RemoveInstance(Intercepted);
DoInvoke := true;
end;
end;
Interceptors.Add(Instance.ClassType, Interceptor);
end;
Interceptor.Proxify(Instance);
end;
function TWeak<T>.GetRef: T;
begin
if FRef <> nil then
case PTypeInfo(TypeInfo(T)).Kind of
tkClass: PObject(@Result)^ := FRef;
tkInterface: PInterface(@Result)^ := IInterface(FRef);
else Result := default (T);
end
else Result := default (T);
end;
procedure TWeak<T>.Zero;
begin
FRef := nil;
end;
procedure TWeak<T>.Clear;
var
Tmp: Pointer;
Obj: TObject;
Intf: IInterface;
begin
if FRef = nil then Exit;
Tmp := FRef;
FRef := nil;
case PTypeInfo(TypeInfo(T)).Kind of
tkClass:
begin
PObject(@Obj)^ := Tmp;
WeakReferences.RemoveReference(Self, Obj);
end;
tkInterface:
begin
Intf := IInterface(Tmp);
Obj := TObject(Intf);
WeakReferences.RemoveReference(Self, Obj);
end;
end;
end;
end.
The wizard and his magic wand example from the TComponent
notification chapter is a perfect fit for demonstrating zeroing weak
reference.
The only difference here is that the Delphi streaming system does not
support generic classes, nor interfaces, and it only supports resolving
references to TComponent descendants. Because of this, we cannot publish
the Wand property as-is. That is the drawback of the weak reference
approach. If streaming support is not important, using the weak pattern
makes code cleaner and simpler.
program WeakMagic;
uses
System.SysUtils,
ZWeakU in 'ZWeakU.pas';
type
TMaterial = (Wood, Stone, Metal);
TMagicWand = class(TObject)
public
Material: TMaterial;
procedure Magic;
function MaterialToString: string;
destructor Destroy; override;
end;
TWizard = class(TObject)
strict protected
FWand: IWeak<TMagicWand>;
public
procedure UseWand;
property Wand: IWeak<TMagicWand> read FWand write FWand;
end;
procedure TMagicWand.Magic;
begin
case Material of
Wood : Writeln('Casting magic with wooden wand');
Stone : Writeln('Casting magic with stone wand');
Metal : Writeln('Casting magic with metal wand');
end;
end;
destructor TMagicWand.Destroy;
begin
Writeln('Wand has been destroyed: ', MaterialToString);
inherited;
end;
procedure TWizard.UseWand;
begin
if Assigned(FWand) and Assigned(FWand.Ref) then FWand.Ref.Magic
else Writeln('Cannot cast magic without a wand');
end;
var
Wizard: TWizard;
WoodenWand, MetalWand: TMagicWand;
begin
ReportMemoryLeaksOnShutdown := true;
// creating wizard
Wizard := TWizard.Create;
// clean up
MetalWand.Free;
Wizard.Free;
end.
Since we don't have any setters implemented for the Wand property - we
don't need any to set a weak variable - our output does not include the
pick-up lines we had in the original TComponent based example.
http://docwiki.embarcadero.com/RADStudio/en/Delphi_Reference
http://docwiki.embarcadero.com/RADStudio/en/Delphi_Language_Guide_Index
http://rvelthuis.de/articles/articles-pointers.html
http://blog.barrkel.com/2009/01/implementing-user-defined-copy-on-
write.html
http://stackoverflow.com/a/861178/4267244
https://en.wikipedia.org/wiki/Tony_Hoare
https://www.infoq.com/presentations/Null-References-The-Billion-
Dollar-Mistake-Tony-Hoare
https://community.embarcadero.com/blogs/entry/a-
andquotnullableandquot-post-38869
Barry Kelly - Virtual method interception
http://blog.barrkel.com/2010/09/virtual-method-interception.html
https://community.embarcadero.com/blogs/entry/exceptional-safety-
28852
Allen Bauer - How can you raise an "Out of Memory" exception when
you don't have any?
https://community.embarcadero.com/blogs/entry/how-can-you-raise-
an-out-of-memory-exception-when-you-dont-have-any-31362
https://stackoverflow.com/a/6321723/4267244
https://community.embarcadero.com/blogs/entry/class-constructors-
popping-the-hood-38899
https://stackoverflow.com/a/5153933/4267244
https://stackoverflow.com/a/4510268/4267244
https://developer.apple.com/library/content/releasenotes/ObjectiveC/RN-
TransitioningToARC/Introduction/Introduction.html
Danny Thorpe - Stack Overflow answer - Is the compiler treatment of
implicit interface variables documented?
https://stackoverflow.com/a/19914385/4267244
https://community.embarcadero.com/blogs/entry/give-in-to-the-arc-
side-38948
http://blog.barrkel.com/2008/07/anonymous-method-details.html
Quality Portal Reports
RSP-10100 Memory Leak on Const-Parameter
https://quality.embarcadero.com/browse/RSP-10100
https://quality.embarcadero.com/browse/RSP-14681
https://quality.embarcadero.com/browse/RSP-14682
https://quality.embarcadero.com/browse/RSP-19204