Delphi’s ongoing problem with "with"

Delphi has long included a scoping construct called with which can be used to increase the readability and efficiency of code.  From Delphi’s documentation:

A with statement is a shorthand for referencing the fields of a record or the fields, properties, and methods of an object. The syntax of a with statement is:
  with obj do statement
or:
  with obj1, …, objn do statement
where obj is an expression yielding a reference to a record, object instance, class instance, interface or class type (metaclass) instance, and statement is any simple or structured statement. Within the statement, you can refer to fields, properties, and methods of obj using their identifiers alone, that is, without qualifiers.

However, with has a really big gotcha: scope ambiguity.  Scope ambiguity came back to bite us yet again today: I am currently upgrading a major project (over 1 million SLoC) from an older version of Delphi to Delphi XE2. One component of this project is a legacy third party component package which is no longer supported.  We have full source and the cost of replacing it would be too high, so we are patching the source where needed.

While tracing a reported window sizing bug, I found the following line of code (on line 12880, yes it’s a big source file):

with vprGetTranspBorderSize(BorderStyle) do
  R := Rect(Left,Top,Width-Right,Height-Bottom);

vprGetTranspBorderSize returns a TRect.  In the earlier version of Delphi, the Width and Height properties were not members of TRect, so were found in the parent scope, being the component itself in this case.  All good.

But now in Delphi XE2, records can now have functions and properties just like a class.  So TRect has a bunch of additional properties, including Width and Height.  Handy to have, until you throw in some code that was written before these new properties existed.  One fix here is simply to qualify the scope:

with vprGetTranspBorderSize(BorderStyle) do
  R := Rect(Left,Top,Self.Width-Right,Self.Height-Bottom);

Or, you could declare a second variable, and eliminate the with statement entirely:

R2 := vprGetTranspBorderSize(BorderStyle);
R := Rect(R2.Left,R2.Top,Width-R2.Right,Height-R2.Bottom);

The problem with the second approach is it makes the code less readable and the introduction of the second variable R2 widens its scope to the whole function, rather than just the block where its needed.  This tends to lead to reuse of variables, which is a frequent source of bugs.

Many developers (including myself) have argued that adding support for aliases to with would avoid these scope problems.  Some others argue that one simply shouldn’t use with, and instead declare variables.  But with does make code much cleaner and easier to read, especially when used with try/finally blocks.

with R2 := vprGetTranspBorderSize(BorderStyle) do
  R := Rect(R2.Left,R2.Top,Width-R2.Right,Height-R2.Bottom);

Given that this issue was reported to Borland as far back as 2002, and since that time many, many complex language constructs have been added to Delphi, I think it’s a real crying shame that Borland/Inprise/CodeGear/Embarcadero have chosen to ignore this problem for so long.

Leave a Reply

Your email address will not be published. Required fields are marked *