Jaap Van Goor was asking on FaceBook about some seemingly strange behaviour when obtaining string representations of Booleans using the Delphi RTL, which led me to revisit some familiar (and some not so familiar) old Delphi ground and take a look at the area involved in further detail.
First of all, let’s quickly deal with the initial “problem”.
Jaap was concerned to understand why ABoolean.ToString returned 0 or -1 rather than false and true (respectively).
The answer is that the Boolean type is not really an enum (though RTTI identifies it as such) but a much more specialised type that has some things in common with an enum but also some quite significant differences.
The symbols true and false are effectively compiler defined constants but with some additional caveats about what one of those particular constants actually means. That is “True” is both a value (1) but also the result of an evaluation of a value, where any non-zero value is “true”.
Unless I’m mistaken, at time of writing it’s still not possible to use ToString on a variable of type enum, where-as you can on an Integer. Which is one clue that Boolean‘s aren’t quite the enums they might claim or appear to be.
A couple of suggestions were made in response to Jaap’s post as to alternative ways to convert a Boolean into a more meaningful string representation. One of these was a function called BoolToStr().
Now, on the occasions when I’ve needed something similar, the needs are usually quite specific to the context of a particular problem. Sometimes you do want simply true and false, other times you might want yes/no and on yet others on/off, enabled/disabled etc
And of course you may want to localise these values.
As a result I have always used a simple Boolean-indexed array for this myself, allowing me to specify the particular values applicable in a particular context:
const BOOLSTR: array[false..true] of String = ('false', 'true'); begin // .. s := BOOLSTR[aBoolean]; // .. end;
If the particular context has some wider scope than a specific block of code, then this of course can itself be wrapped up inside a helper method of my own, itself scoped as appropriate to the context.
But I didn’t even recall ever having heard of BoolToStr() before, so I took a look.
As a result I’m wondering now whether I had in fact heard of it, looked into it and then deliberately purged it from my memory!
Brace yourselves, this isn’t going to be pretty.
Here be Dragons
BoolToStr() accepts up to two boolean parameters. The first is the one you wish to convert to a string representation and the second indicates whether you want to “UseBoolStrs“. If you indicate that you do not (the default if not specified) then you get 0 or -1:
function BoolToStr(B: Boolean; UseBoolStrs: Boolean = False): string; const cSimpleBoolStrs: array [boolean] of String = ('0', '-1'); begin if UseBoolStrs then begin VerifyBoolStrArray; if B then Result := TrueBoolStrs else Result := FalseBoolStrs; end else Result := cSimpleBoolStrs[B]; end;
So far so simple. Well, down that particular conditional branch anyway. If UseBoolStrs is false then the result is simply plucked from a Boolean-indexed array.
Where things get interesting and – frankly – messy and not a little bit scary, is if you have said that you do indeed wish to UseBoolStrs.
In that case then the RTL goes scurrying off down a path that takes us through VerifyBoolStrArray (every time) and then returning the first value in one of two separate arrays, conditional on whether we’re converting a true or false value.
VerifyBoolStrArray itself simply checks whether the length of these two arrays is not zero (i.e. empty) and if either one is then sets an length of 1 with an initial, non-localized default value in each (the constants DefaultTrueBoolStr and DefaultFalseBoolStr are defined as ‘True’ and ‘False’).
procedure VerifyBoolStrArray; begin if Length(TrueBoolStrs) = 0 then begin SetLength(TrueBoolStrs, 1); TrueBoolStrs := DefaultTrueBoolStr; end; if Length(FalseBoolStrs) = 0 then begin SetLength(FalseBoolStrs, 1); FalseBoolStrs := DefaultFalseBoolStr; end; end;
Also of some interest is that in the TryStrToBool() function the implementation allows for the possibility that these arrays may have more than just the one entry introduced by the Verify function.
But nowhere else in the entire RTL does any code modify or add to these arrays.
Nothing adds to them, nothing changes them. Nothing.
And the strings placed in the arrays are just regular string constants, not resourcestring, and are specifically commented as not to be localised.
On the face of it then, it is all just unnecessarily convoluted and could simply be replaced by a simple lookup from a similarly boolean-indexed array:
function BoolToStr(B: Boolean; UseSymbolicStrs: Boolean = False): string; const cStrs: array [boolean, boolean] of String = ( ( '0', '-1'), ('False', 'True') ); begin result := cStrs[UseSymbolicStrs][B]; end;
(Ok, so I admit to a little additional finesse here. :))
But it bothered me.
Why go to all this trouble if the arrays are only ever going to be setup in a particular way… ?
Which led to a worrying thought and the realisation of the true horror lurking within BoolToStr(). The reason for consulting these arrays rather than simply directly using the constants held within them, as well as for allowing that even the length of the arrays might vary….
They are Unit Interface Variables.
Or as some people like to call them: Global Variables.
Anybody is free to add extra items to those arrays or change the values of any strings that might already be there.
Fancy messing with a colleague’s Friday afternoon ? Simply check-in this code somewhere in the initialization of a unit:
initialization if not assigned(TrueBoolStrs) then SetLength(TrueBoolStrs, 1); if not assigned(FalseBoolStrs) then SetLength(FalseBoolStrs, 1); TrueBoolStrs := 'False'; FalseBoolStrs := 'True';
Then sit back and wait for the hilarity to ensue. (Of course your code review process will spoil your fun, but you get the point). 🙂
More seriously, this of course (we must presume) to allow you to come up with your own alternative replacement strings for True and False for conversions. Alternatives which you wish to use across the entirety of your application, in every nook and cranny where such conversions take place.
You can even define multiple strings from which Booleans can then be converted (by StrToBool and TryStrToBool), although only the first one in each array will be used when converting back the other way, as we have seen.
Of course, to do that you need to check to see whether any RTL code might have already initialised those arrays and then just change or add the values you need, or you may need to initialise the arrays entirely, if the RTL has not yet initialised them.
That VerifyBoolStrArrays() function is the one implementation detail that is private to the SysUtils function though unfortunately, so you’ll have to do all that the long way around.
These complaints aside, initially this might seem quite useful, until you realise that this means that if you rely on this yourself, then you must also always be aware that every time you use some 3rd party library or framework there is a chance – however slim – that they too thought it would be a great idea to set up their own custom strings in those arrays and that their idea of what makes for really useful strings isn’t the same as yours.
So not only is this convenient looking little utility method horrendously inefficient, all that overhead to make it possible necessarily involves exposing any consumer code to potential risk of side effects arising from sharing code.
Retrospective / Legacy
Lest there be any confusion, this isn’t some newly introduced foible. It has been this way – literally, identical in every respect – since at least Delphi 7 and current as of at least Seattle.
This one’s simple: I think if you don’t mind I’ll stick to my Boolean-indexed arrays. 🙂