by Peter Wone - Wamoz Pty Ltd
I was bagging Visual Basic one day, telling Mark that "...the reason [he] can't get many articles for Delphi is that most articles deal with overcoming the limitations of the tool, and Delphi doesn't really have any [limitations], so there aren't any articles about overcoming them. Visual Basic, on the other hand, is one huge collection of things you can almost do. More than half of any given VB program is workaround code unrelated to the task at hand. This doesn't happen in Delphi..."
That statement stands, except that I should have said "...Delphi doesn't have many [limitations]..."
But Mark (rotten sod that he is) seized upon the implication in my statement that Delphi is perfect, and used it to challenge me to either defend that position in an article on Delphi, or write a piece about the various problems I've encountered and what I did about them.
As it happens, I keep a journal (there's a growing pile of them on my bookshelf) in which I record exactly this sort of thing. The contents of my notebooks are actually largely abstract stuff about data modelling, much of which turns out to be more developed re-statements of earlier observations, but there are a few Delphi pearls.
I've rifled through the top couple of notebooks, and here's a sort of best-of.
Before you get lost in the details, remember -
Get the message
Last issue, in my column Toybox, I made a lot of noise whining about the need to implement messaging in TObject so that all classes would be capable of messaging. I called on Borland and Sun and even the forces of darkness (that would be Microsoft) to do something about this.
Talk about embarrassing - Borland already did this in Delphi. In version one. In the base class, TObject, there are two methods which work in tandem.
TObject.Dispatch(var Message); virtual; TObject.DefaultHandler(var Message); virtual;
Both are designed to be overridden.
How it works
Excerpted from the Delphi help:
Dispatch determines whether a message is in the list of message handlers declared for the object. If the object does not handle the message, Dispatch then examines the message-handler list of the ancestor type, and continues checking ancestors until it either finds a specific handler or runs out of ancestors, in which case it calls DefaultHandler.
DefaultHandler is called by Dispatch when it cannot find a method for a particular message. DefaultHandler provides message handling for all messages for which an object does not have specific handlers. Descendent classes of TObject that process messages usually define DefaultHandler methods according to the type of messages they handle. For example, TWinControl overrides DefaultHandler to call DefWindowProc.
Calling inherited in a message-handling method results in a call to the ancestor's DefaultHandler method only if that ancestor does not specify a message method for the particular message being handled. Otherwise calling inherited results in a call to the specific handler for that type of message.
The "declared message handlers" should be familiar. At some stage you've undoubtedly written something like this:
type TEditMutant = class(TEdit) ... protected procedure RightButtonDown(var Message:TWMMouse); message WM_RBUTTONDOWN; ... end;
I certainly have. What I didn't know was that invoking an object's Dispatch method would trigger handlers bound like this.
How do I get Windows messages?
The simplest way is to derive, directly or otherwise, from TWinControl.
The overhead of a TWinControl in every object? Don't be silly. Derive a proxy from TWinControl and use the Broadcast method to offer the message to all of the parented controls. If you don't like the shotgun approach of Broadcast then make you proxy a little fancier and keep track of "focus".
As far as Windows is concerned, non-windowed controls can't have focus (display a carat and receive keyboard messages). You need a window handle for that, simply because keyboard input arrives via the windows message queue. Since you are extending messaging to your controls, thay can have the logical focus.
You'll have to implement everything yourself, but at least you can. Anyway, the point of all this is minimalism, so it shouldn't be prohibitive. And we're talking about a real language, not some bloated descendant of a scripting language. Implement your standard stuff protected in a base class and just inherit.
Update tracking for shared objects.
Rather than notify each client of a shared model object, defer this until a client decides to use the updated information. Don't forget that when your view object is also a client.
The model should keep a record of the last client to update it. All its update methods should require a signature value for the object which initiates the update. This may occur several levels of indirection from the model object; the indirection methods should merely pass the signature through.
My favourite for an object signature is a pointer to the object. Generally I make the signature member of the model object an integer, and typecast the pointer to an integer before passing it. This removes the temptation - that many people apparently can't resist - to use the pointer as a pointer. Silly people - you can't know its origin or type. A highly unsanitary practice, to be discouraged. But in terms of small light process-unique identifiers that can be obtained quickly and easily, they can't be beaten.
The signature passed in by methods which modify the model object should be written into a member variable FSignatureAtLastWrite immediately by whatever method handles the update. The model object should publish a method ReadRequired -
function TModelObj.ReadRequired (const EnquirerSignature, SignatureAtLastRead:integer):boolean; begin Result := not ( (FSignatureAtLastWrite = EnquirerSignature) or (FSignatureAtLastWrite = SignatureAtLastRead)) end;
Clients need to keep track of the last write signature (TModelObj should offer it in a property Signature). Then all you need to do is make update dependent operations conditional on ReadRequired().
if aModelObj.ReadRequired(EnquirerSignature, SignatureAtLastRead) then ReReadModelObj; DoSomethingUpdateDependent;
Another way of doing this that requires less action on the part of the clients uses timestamps.
Timestamps frequently pose a problem in distributed systems due to clock drift. In this strategy, only one clock is in use - the clock local to the shared object. However, while this strategy no longer requires clients to sign their writes, clients can no longer omit reading when they were the last to write, because this strategy doesn't record who wrote, only when a write occurred, and the responsible client doesn't know the value of the client's clock at the moment of writing.
One school of thought says that the cost of opening a socket is so high that it is cheaper to have the server notify the client of the signature time after a write, in order to avert an unnecessary connection later.
Access protected properties of common ancestors by sloppy typecasting.
If you aggregate to emulate multiple inheritance, but the type of the aggregated ancestor is indeterminate because it's dynamic, and you want access to a property they have in common but which is not public in the common ancestor, what do you do?
If you play by the book, you write a heavily type dependent case statement to select an appropriate typecast, and then you have nightmares for years about the grief of maintaining it.
If you're sly, you realise that the fact that the protected method or property comes from a common ancestor means that VMT furnished addresses and interface prototypes will be consistent in all descendents of all branches for methods and properties which are not overridden.
So you can typecast as anything that exposes the method or property
For example, say you want the Text property of something that descends from TWinControl, which introduces Text as a protected property, but the only thing you know about the object on the end of the pointer you have is that it descends from TWinControl. As what do you typecast it? Happily, as long as you just want to read it, you can use TEdit. Why TEdit? Mostly because it publishes Text.
You can also write into it. Safely? We-e-ell, that particular piece of code won't go bang. What may bite you is that you may accidentally bypass an override of the method which would have performed some sort of value checking.
It's your problem. If your class hierarchy is such a mess that you can't find a suitable common ancestor, or at least create one, then I put it to you that it is high time you did some serious refactoring.
And don't tell me that your schedule doesn't permit it. I've had quite enough of this never time to do it right, always time to do it again stupidity. Good quality software takes about three tries. This should be factored in the costings, but companies err on the side of hazard in order to look good on tenders.
If your boss starts to give you a lecture about deadlines, point out to him that electing not to perform preventive maintenance makes your company liable for consequential damages, and ask him whether he thinks your company is likely to refrain from dumping it all on the person who made the decision.
Pride and prejudice
The right way to do case insensitive string comparison.
If I see one more code fragment like this
if UpperCase(AString) < UpperCase(AnotherString) then DoSomething1 else if UpperCase(AString) = UpperCase(AnotherString) then DoSomething2 else DoSomething3;
I'm going to scream. Or perhaps not; it wouldn't bother you even slightly. Ok, if I ever see another code fragment like that I'll take all my clothes off, stand on the junction of our cubicles and sing. You have been warned.
To avoid this horrible fate, try the following alternate structure. If your variables are declared as string rather than PChar then you'll have to typecast them, but it's very much more efficient than case conversion. I usually declare a function to support this.
If you're wondering what "Pride and Prejudice" has to do with string comparisons, it's just that I have considerable prejudice about the right way to do things, and too much pride to admit any other possibility.
function StringIComp(const s1,s2:string):integer; begin Result := StrIComp(PChar(s1),PChar(s2)) end; case StrIComp(AString,AnotherString) of -1:DoSomething1; 0:DoSomething2; 1:DoSomething3; end;
The shameless pointer
Getting an array of byte into a typed variable
I can't believe the contortions people code through to do this. It's easy to bypass type checking.
type TType = record //blah blah blah end; PType = ^TType var MyPType:PType; MyType:TType; //(do something to get AByteArray) MyPType := TType(pointer(AByteArray)); MyType := MyPType^;
MyPType is a pointer. You refer to the value as MyPType^. You can typecast a pointer to anything. This is the most common sensible bypass of type checking but there are other occasions.
Storing an ordinal in TStringList.Objects[n]
Also a convenient abuse of ObjectPascal is the use of pointers to store ordinals.
var i:integer; AStringList:TStringList; ch:Char; s1,s2:string; AStringList.Objects := pointer(i); //store i ch := Char(Ord(AStringList.Objects)); //fetch i as a char i := integer(Ord(AStringList.Objects)); //fetch i as an integer AStringList.Objects := pointer(s1); //points at s1 s2 := string(AStringList.Objects^);
Strictly the typecast to integer is unnecessary. I just did it to emphasise the fact that once you wrap Ord() around it you can do what you like.
This is a quick and dirty thing to do. I only do it because I'm lazy. Later on I generally hunt down and use a unit that declares TStringListOrd, which overrides the object get and set methods and performs the typecast automatically.
Something else you can do is point at variables (besides object variables) as illustrated with s1 and s2.
Variations in D minor
DataSet.Locate, variants and an undocumented "feature".
According to Borland documentation, the Locate method of a TDataSet takes a semicolon-separated string defining the keyfields, and a variant array of values to match, one for each keyfield. This is true, but it's not complete.
Firstly, for some reason variants don't autocast when used with DataSet.Locate. You have to ensure the variant elements of the array have the same variant type as the field with which they will be compared, otherwise you'll get an invalid variant type conversion error. You do this by typecasting when assigning the variant array element.
Secondly, in the case where there is only one keyfield involved, you might expect that you would pass a variant array containing one element. If you do that, you'll generate an invalid variant type conversion error. For some bizarre and thoroughly undocumented reason, Delphi wants a simple variant, which is not the same as an array with one element.
Borland must have coded explicitly to achieve this behaviour. I can see why they would; when there's only one key field involved it saves you from the not inconsiderable overhead of using VarArrayOf(). Pity they didn't document it explicitly.
Here's another, related glitch. The following will result in v having a result of Unassigned.
var v:variant; v := VarArrayOf(); //variant array with one element v := v; //should mean v = 1 but v = Unassigned
This is because during the assignment process, while preparing to store 1, the variant type is changed from (varArray or varVariant) to varInteger, and the array is deallocated before the value is copied. You have to use a temporary variable.
var va,v:variant; va := VarArrayOf(); //variant array with one element v := va;
This way it works.
My turn to be puzzled
Can anyone tell me why this is illegal?
var dset1,dset2:TQuery; begin try dset1.Create(nil); dset2.Assign(dset1); finally dset1.Free; dset2.Free; end; end;
I went through dbtables.pas and it appears that Assign is inherited all the way from TPersistent without being overridden, but that should simply result in the protected method AssignTo() being called. The code above causes an access violation. If Borland didn't intend TQuery to be copied like this then surely they should have raised a nice clean exception instead of letting it go bang.
Creating dset2 before assigning it doesn't help.
And some really truly compiler bugs
I had a cunning plan, and with it I out-clevered ObjectPascal, and, as a consequence, myself.
Normally I'm a big fan of Borland's smart-linking technology, but the other day it bit me in a fascinating way.
I had a Singleton object, and to ensure that it wasn't created unless required, and automatically created before use, all references to the object were courtesy of a function. The function was of the Singleton's class, and looked at a unit level variable. If the function was accessed and the variable nil, it created and assigned an instance to the variable. After the check, it returned a reference to a valid object.
All very clever and logically quite valid. But for some of the properties of that class there weren't any explicit references via instances of the class, which meant that there weren't any entries for them in the name tables during compilation. In consequence, the smart linker decided those properties were dead code, and removed them.
Surprisingly, the code which used the Singleton didn't generate exceptions at run-time.
What cottoned me on to the nature of the problem was that one of its properties was itself an object, and it steadfastly remained nil, against all my craft and suasion.
I could trace up to the point where a pointer was written into the property, and right over the assignment, yet immediately after assigning it, the property was still nil. Eventually I decided to write get and set handlers instead of connecting the property directly to a member variable. I breakpointed the assignment, compiled, and Lo! the compiler announced that it wasn't a valid breakpoint.
Aha! thought I, the compiler has decided this is dead code. So I tried deliberately making explicit reference to the property inside the Singleton unit, and suddenly all was sweetness and light.
The path to enlightenment
If you've ever specified a source code path in project options, you probably used the same syntax as the default paths to the Borland libraries, which is to say paths not terminated by backslashes. Generally speaking, this works fine.
But if you have a chain of inheritance for forms, and the ancestor forms are in a shared library on a server (ie not in the same place as the rest of your source) and you check an ancestor out of the source tree and modify it, then the next time you load the project into Delphi, you may get messages about Delphi not being able to locate the ancestors for forms which descend from the form you are working on.
If this happens, you will also find that, for the forms for which the error was raised, while you can edit the source code and can compile without problems, you cannot switch to form view.
The solution is to terminate all the search paths with a backslash, make this the default option, save the project and restart Delphi. The problem will cease to manifest.