Tom Blocker wrote:
> Explicit or untested casts don't make sense since the language gives
> us an easy way to not lay such landmines. I never let this one get by
> in code reviews. It is just too dangerous.
> 
> A last note, using interfaces can also provide an alternative to
> doing a bunch of such casts thereby improving coupling and reducing
> dependencies. GetInterface basically does this very thing, test then
> use...
> 
> Rob, what am I missing here?

First off, we're starting with the premise that the function in question 
is always supposed to receive an instance of TMyForm, right?

Whether the function is actually declared that way, and whether all 
callers follow that rule, aren't certain, but *if* a caller passes 
something that is not a TMyForm, then we should consider the program to 
have a bug -- it should be fixed to either pass an object of an 
appropriate type, or not call our function at all.

You seem to think that I was suggesting writing code like this:

procedure Foo(F: TForm);
begin
   TMyForm(F).Test := 0;
end;

That's not what I suggested, though. It doesn't do anything to prevent 
the bug condition. The caller can pass any TForm, and the function will 
simply assume it's of the desired type. Finding that bug will take a 
long time.

Ideally, I'd prefer this:

procedure Foo(F: TMyForm);
begin
   F.Test := 0;
end;

That completely eliminates the need for testing the input's type at run 
time. The compiler performs all the tests for you.

But suppose for some reason you can't write that ideal function. Maybe 
the function's signature is dictated by another interface, which expects 
a particular function-pointer type. There are ways of ensuring that the 
function still always gets an instance of TMyForm.

One is to use an assertion:

procedure Foo(F: TForm);
begin
   Assert(F is TMyForm);
   TMyForm(F).Test := 0;
end;

Now the function will reject any incorrect input. The caller can still 
pass objects of the wrong type, but the function will raise an exception 
at run time, so we'll be able to see immediately where the problem is 
the first time we test the program.

We can also use a checked type-cast:

procedure Foo(F: TForm);
begin
   (F as TMyForm).Test := 0;
end;

This differs slightly from an assertion. Assertions can be disabled, but 
checked type-casts can't, so this latest function will always check its 
input, even in release builds that have assertions turned off. Also, the 
exception type will differ. That's probably not an issue, though, since 
neither EAssertionFailed nor EInvalidCast are exceptions that anyone 
should have much reason to catch. Either way, these latest two functions 
either set the Test member to zero, or fail with an exception, which can 
be noticed during debugging.

Now we modify the function to make it look like your example:

procedure Foo(F: TForm);
begin
   if F is TMyForm then
     (F as TMyForm).Test := 0;
end;

This function will never fail with an exception. If its input is not an 
instance of TMyForm, the function will simply return to its caller as 
though nothing happened. I consider that a bad thing. The premise of 
this exercise is that it is an error in the program if this function 
gets called on non-TMyForm objects. But the way this function is 
written, it provides no help in detecting that error. We need to perform 
much more thorough testing to find it. We need to test the value of the 
Test member on output to ensure it's been set. Once we notice that it's 
not being set, we need to step through the code to determine why.

The code performs a redundant test. Like I wrote in my previous message, 
the above function works identically to this:

procedure Foo(F: TForm);
begin
   if F is TMyForm then begin
     if F is TMyForm then
       TMyForm(F).Test := 0
     else
       raise EInvalidCast.Create(...);
   end;
end;

But as I just explained, that code will never actually raise an 
exception. The only way it could raise an exception is if a test that 
succeeds the first time fails the second time. But that can't happen, so 
let's simplify the code by removing the "else" branch. (The compiler 
can't remove it form the code it generates for the "as" operator, but we 
can remove it from our manually written equivalent.)

procedure Foo(F: TForm);
begin
   if F is TMyForm then begin
     if F is TMyForm then
       TMyForm(F).Test := 0;
   end;
end;

See the redundancy? There's no reason to invoke two "is" tests with 
identical arguments in sequence like that. So let's move the second one:

procedure Foo(F: TForm);
begin
   if F is TMyForm then begin
     TMyForm(F).Test := 0;
   end;
end;

When I wrote, "The 'as' operator performs an 'is' test anyway," maybe 
you thought I was suggesting removal of the "is" test, but that's not 
what I meant. I was suggesting removal of the "as" cast since the result 
of the test it performs is already known and doesn't need to be 
performed again.

The code you posted performs unnecessary checks, and it fails silently. 
I consider those good reasons not to use that style of code. I gave my 
preferred styles earlier in this message.

-- 
Rob
_______________________________________________
Delphi mailing list -> [email protected]
http://www.elists.org/mailman/listinfo/delphi

Reply via email to