
The topic of RAII in Delphi is usually silenced or information on this issue is limited to discussing the usefulness of interfaces. But interfaces one by one do not provide many of the desired features. When in Delphi 2006 there was an overload of operations, private fields of records, own constructors and methods in records, and it seemed it would be logical to see the automatically called destructor. Both run-time allows, and in
the request section for new features of Delphi over the course of several years, query
№ 21729 “Record Operator Overloading: Please implement“ Initialize ”and“ Finalize ”operators” has been in TOP-10. Probably not destiny. Nothing, I will show how to do without failed features. Since Delphi 7 is very much alive, solutions compatible with Delphi 7 will be considered, including
Time to find workarounds was enough. This article is not a tutorial and is intended for advanced Delphi developers who write their own libraries or bind to libraries in other programming languages.
Why do you want RAII in Delphi?
')
- Automatic memory management. The staircase from try ... finally is not serious. TList, TComponentList and others require discipline from who will apply them. It is especially difficult, without using automation, to make the correct release of memory for variables used from the ascending circuit.
- Copy-on-write and reference counters
- Other special copying behavior
- Copy-on-write and reference counters for objects created in third-party libraries (for example, CFString)
For which types of Delphi does automatic control work?
- AnsiString, WideString, UnicodeString - strings
- array of ... - dynamic arrays
- reference to ... - closures in Delphi 2009+
- interfaces
- Variant, OleVariant - options
Among all these features, only interfaces and options are programmable.
What are the bad interfaces?
- Nil is initialized and no methods can be called.
Not suitable if you want to implement your own type of string or your own long arithmetic. An uninitialized variable should behave as an empty string or 0, respectively. - Methods cannot change the contents of the variable – pointer for which they were called
- There is no control over what happens when copying an object. Only AddRef that cannot change the contents of a pointer variable.
- There is no built-in ability to make copy-on-write
- No overload operations
What are the bad options?
- Unassigned is initialized, which also cannot call methods
- Challenges untyped. Implementing IDispatch or dispatch options is a non-trivial and poorly documented area of ​​expertise.
- The need to implement dreary conversions between other types of options, all kinds of auxiliary methods that can be caused
How to solve most of these problems?
The solution I propose is to wrap interfaces or variants inside the private part of the record. We declare the type of record. We declare the type of interface. We duplicate all the methods in the interface and in the record. Recording methods redirect all calls to an internal object, and you can do what the interface type variable itself cannot do.
In the implementation of each write method, we consider the case when the private field is nil - it may be necessary to automatically initialize the object before invoking something from it. If you need to implement Copy-on-write, the method is declared in the interface.
procedure Unique(var Obj: IOurInterface);
This method determines its uniqueness by the reference counter. If the object is not unique, the object must create its own copy and write this pointer in Obj instead of itself. Each write method that can change something must ensure that the pointer is unique before transferring control to the interface method. For internal needs, it is possible to provide var Obj: IOurInterface with other interface methods. For example, by analogy with built-in strings, there may be a desire to make it so that when there is no character left in a string of its own type, the dynamically allocated object is deleted, and the internal pointer becomes nil
In order to optimize, when implementing your own strings or long arithmetic, it may be necessary to consider the special case a: = a + b. I can not guarantee that it will work, but you can try when implementing the operation + compare the @ Self and @ Result pointers
The problem of unconditional automatic initialization of the internal field is fundamentally insoluble, but it can be initialized at the first call. The remaining problems are solvable either by wrapping the interface into a record, or by wrapping the option into a record, more on that later.
The record option is like a marshmallow in the glaze, but the record option
Own type of option gives more complete control in comparison with the interface. Since the variant field is private and outside this option should not leak, it is possible to implement only a minimal set of methods of your own (custom) type of option. Apart from a debugger trying to cast (CastTo) a variant to a string, when you hover the cursor, you will need to implement a copy (Copy) and destroy (Clear) variant. In operative memory, own types of a variant, as a rule, consist of a variant type marker and a pointer (for example, a TObject descendant). How this is done, I propose to look at the example of the implementation of complex numbers (VarCmplx.pas), which is present, at least since Delphi 7
Using variants would be useful for a single-stranded wrapper CFString. If you do a wrapper for the interfaces, Delphi will call AddRef and Release on the interface, but CFString is not an interface, and you will need to either wrap CFString into an additional layer of indirection from the interface, or use your own type of variant that causes CFRetain and CFRelease required for normal memory management. CFString. This would work much better than the CFString wrapper that Embarcadero offers in Delphi XE2.
Hey, what about Delphi 7?
Delphi is a language with a long history, and before the Delphi object system appeared, there was another object system in Borland Pascal with Objects. In Delphi 7 and Delphi 2005, it still functions. Instead of a record, the object keyword is written, and the resulting type is in many ways similar to the record in Delphi 2006: it may have private fields, it may have methods. Objects of the same type can be assigned to each other, in this sense they are also analogous to record. Just what we need. The compiler will swear on the unsafe type, there is no overload of operations, but this is the only inconvenience. The similarity of object and record is so great that it is possible, using conditional compiler directives, on older versions of Delphi to declare a type as object, and on new ones - as a record. That's exactly what I did in my small
Delphi-CVariants collection library
.Problems can arise if one tries to declare several such types using each other. Cyclic dependencies in the source code are provided for classes, interfaces, and pointers, but not for objects as is. It is preferable to declare objects so that each next knows about the previous ones, but not vice versa. Therefore, for example, in my library, CMapIterator knows about CVariant, but CVariant does not know about CMapIterator.