In the previous article, I mentioned that Delphi includes a 'with' statement, and although it seems like an unimportant for a language to have, I'm going to teach you something that will make you stand back and give you an alternate perspective on this simple mechanism. Sorry all you C++ fans, this power tip applies only to languages that have a 'with' statement.
With statements are the most useful for one thing: 'with'-helpers.
What is a 'with'-helper?
Yeah, I knew you were thinking that: a 'with'-helper is a class or data structure that is designed to be more convieniently used via a 'with' statement in Delphi. It's one of the most Ruby-like features that Delphi has to offer, brining a greater increase of convenience and fun to Delphi programming. The most quick way to demonstrate is through examples, so come on, let's take a look...
Let's start with a simple example: a work-around for variables. In Dephi, one normally has to explicitly declare variables before the block of code that uses them begins, like so:
procedure ReportMyError(Code: integer); var //we're declaring the variable here in the "var" block vMsg: string; begin if Code > 0 then if GetErrorMsgStr(Code, vMsg) then PrintLn('Error!'#13#10 + vMsg); end;
This can quickly become inconvenient, so, why don't we create a 'with'-helper that provides a temporary string variable:
type TStringValue = record Value: string; end; function StringValue: TStringValue; begin Result.Value := ''; end;
And then use it like so:
procedure ReportMyError(Code: integer); begin if Code > 0 then //calling the StringValue() function to create a new string value, //which is put into the context with the "with" statement: with StringValue do if GetErrorMsgStr(Code, Value) then PrintLn('Error!'#13#10 + Value); end;
Did you notice where identifier called 'Value' came from? 'Value' was implicitly referenced from the TStringValue record returned by the StringValue() call and implicitly put in scope by the 'with' statement. Get all that? Thanks to Delphi's built-in support for records, we don't do any memory management! Yay! Delphi is better!
We can do much better than this, however. Let's create a nice 'with'-helper for providing the effect of a 'let' statement: create and compute a value, sort of like a closure:
type TFloatValue = record Value: extended; end; function FloatValue(AValue: extended): TFloatValue; begin Result.Value := AValue; end;
Now we use it:
function EmitRatios(A, B: extended); begin with FloatValue(Sqrt(Sqr(A)+Sqr(B))) do begin PrintLn(FloatToStr(Value/A)); PrintLn(FloatToStr(Value/B)); end; end;
Sweet! The value passed to FloatValue() was only computed once! Yay for the extra efficiency boost! Records can do some good and come in handy-dandy using them. Along with what we've seen, we can also return multiple results from functions, capture them with a 'with' statement, and leverage them thus. But there's one problem with this last example: records can only do so much: their fields are mutable and cannot be protected from writing to them. This could cause horrible bugs in a program if we're not careful. Thus, we move on up to using classes as 'with'-helpers. Here's our previous example modified:
type TFloatValue = class private FValue: extended; public property Value: extended read GetValue; end; function FloatValue(AValue: extended): TFloatValue; begin Result := TFloatValue.Create; Result.FValue := AValue; end; //using it: function EmitRatios(A, B: extended); begin with FloatValue(Sqrt(Sqr(A)+Sqr(B))) do try PrintLn(FloatToStr(Value/A)); PrintLn(FloatToStr(Value/B)); finally Free; end; end;
Notice the try-finally block for freeing the created class called by and provided for the 'with' statement. Delphi knows that the structure used by the 'with' statement should have its members in the enclosing scope, so this technique will also work even if the 'with' statement is already inside a class method.
Now that we're using classes, we can do more sophisticated stuff, seeing as classes provide not only properties, but methods. As a last example:
type TTokens = array of TToken; procedure Append(AToken: TToken; var Tokens: TTokens); begin SetLength(Tokens, Succ(Length(Tokens))); Tokens[High(Tokens)] := AToken; end; function ScanTokens(AStr: string): TTokens; begin SetLength(Result, 0); //constructs a TScanner class, giving it the //string to scan: TScanner = token iterator with Scanner(AStr) do try while ReadNextToken do //TScanner.ReadNextToken method Append(CurToken, Result); //TScanner.CurToken property finally Free; end; end;
Thanks for learning. Hope you enjoyed this power tip. Happy coding!