paradise
paradise

Reputation: 491

How to replace empty except block?

Browsing some legacy code, I encounter some "empty" except blocks. They all were implemented for a similar reason, that is to handle a conversion from a text in TEdit to a numeric value. As the TEdit might be empty, there should be no error message in such a situation:

procedure TmyForm.EditExit(Sender: TObject);
begin
  ...  
  try
    _value := StrToFloat(Edit.Text);
  except
  end; 
  ...
end;

That works well but not really a good practice I guess. Is there a better way to get the same behavior?

Upvotes: 2

Views: 772

Answers (2)

Disillusioned
Disillusioned

Reputation: 14832

TLDR Don't seek a 'one size fits all' blanket replacement option for the exception swallowers. Rather identify the specific problems you're trying to address in each case and ensure your code explicitly conveys its intent.


You may already know this, but I want to highlight some problems in the code presented. Understanding these problems yields distinct possibilities in what could go wrong. It helps to think about what behaviour you want in each case, and you should ensure your code explicitly reflects your decisions making it easier to read and maintain.

begin
  ...  
  try
    _value := StrToFloat(Edit.Text);
  except
  end; 
  ...
end;

The reason for the exception swallowing is explained as:

As the TEdit might be empty, there should be no error message in such a situation.

  1. With reference to the quoted comment: the exception swallower is hiding more than the stated intention. Invalid characters or a value that 'doesn't fit' and cannot be stored, would also raise exceptions that are summarily swallowed. This may be acceptable, but my guess is that these possibilities were more likely overlooked. Although far less likely, even exceptions totally unrelated to the conversion itself would be swallowed: such 'out of memory' or 'stack overflow'.
  2. If an exception is raised in StrToFloat then assignment to _value is skipped. So it retains whatever its previous value happened to be.
    • This is not necessarily bad if its previous value is predictable and its previous value is acceptable for purpose.
    • But the value might not be predictable. Especially if it's an uninitialised local variable: it will have whatever happened to be on the stack at the time of the call. But you might also find that if it's a member of an object that it has a predictable but very difficult to discern previous value - making the code hard to read.
    • Even when it seems predictable, that value might not be "fit for purpose". Consider a form reading edit control values into properties on a button click. On first click the value was valid an property set. But on second click, value has been changed and property is not updated - yet it holds the clearly no longer valid value from the first click.
  3. Finally, the most important concern is that if something goes wrong, the method that swallowed the exception cannot perform its task correctly. And other code that called the method (yet presumably relies on its correct behaviour) is blissfully unaware of the problem. Usually, this simply leads to a delayed error (which is much more difficult to fix). The root problem is hidden, so something goes wrong later. E.g.
    • Another method called the above expecting _value would be correctly assigned for some calculations.
    • The root error is hidden, so _value is incorrect.
    • Later calculation might produce EDivByZero or simply an incorrect result.
    • If incorrect result is persisted, the problem could remain hidden for years.

As mentioned before, how you fix the exception swallowers depends on your intent in each case (yes it's more work this way, but if you're fixing something it helps to fix it "properly").

Option 1

Do you really need to hide the fact that the "user made a mistake"?

Assuming there are no other error handling mistakes in the code:

  • If user types "Hello" an exception will be raised, aborting the current operation. The user will see an error message stating 'Hello' is not a valid floating point value..
  • Similarly not entering any value will result in: '' is not a valid floating point value..

So certainly an option worth serious consideration is to: Simply delete the swallower.

begin
  ...  
  _value := StrToFloat(Edit.Text);
  ...
end;

Option 2

Since you're specifically concerned about raising an error when the TEdit empty you may be able to consider this as a special case.

  • It might make sense to assume that when the user doesn't provide a value, 0 is a suitable default. NB! Only do this if it really is an appropriate default. (See next point)
  • The value might be optional. In which case you don't want to report an error if something optional is not provided. Rather set a flag associated with the value that indicates it was not provided. (NB: Don't overload a valid value to indicate 'value not provided'; otherwise you won't be able to differentiate the two.)

In either case it's worthwhile handling the special case explicitly and allowing default error reporting to kick in otherwise.

begin
  ...  
  if (Trim(Edit.Text) = '') then
    _value := 0
    //or _valueIsNull := True;
  else
    _value := StrToFloat(Edit.Text);
  ...
end;

NOTE: You could of course also set a default of 0 in the control value in advance of the user updating controls. This makes the default clear to the user. And if the user chooses to delete the default, then reverting to option 1 informs the user that this is not allowed.

Option 3

The default error handling can be made more user-friendly. E.g. You might want a more informative message, ensure focus is set to the 'error control', or set a hint message.

In this case you'd want to use TryStrToFloat() as per David's answer. As far as I can tell this was introduced in Delphi 7. (In older versions you may be able to use TextToFloat(). Examine the implementation of StrToFloat() for an example.)

The following is but one possible example to demonstrate how you could write some simple utility functions to encapsulate your specific needs and make your special handling of certain cases explicit.

type
  TDefaultMode = (dmNone, dmDefaultEmptyValue, dmDefaultInvalidValue);

function ReadFloatFromEditControl(AEdit: TCustomEdit; ADefaultMode: TDefaultMode = dmNone; ADefaultValue: Double = 0.0): Double;
begin
  if not TryStrToFloat(AEdit.Text, Result) then
  begin
    if (ADefaultMode = dmDefaultEmptyValue) and (Trim(AEdit.Text) = '') then
      Result := ADefaultValue
    else if (ADefaultMode = dmDefaultInvalidValue) then
      Result := ADefaultValue
    else
    begin
      AEdit.SetFocus;
      raise EConvertError.Create('['+AEdit.Text+'] is an invalid floating point value.');
    end;
    {If default is applied, replace value in edit control}
    AEdit.Text := FloatToStr(Result);
  end;
end;

{Use as follows:}
v1 := ReadFloatFromEditControl(Edit1);
v2 := ReadFloatFromEditControl(Edit2, dmDefaultInvalidValue);
v3 := ReadFloatFromEditControl(Edit3, dmDefaultEmptyValue, 1.0);

Upvotes: 4

David Heffernan
David Heffernan

Reputation: 612794

You should use TryStrToFloat:

if TryStrToFloat(Edit1.Text, _value) then
  // do something with _value

This is a function that returns a boolean indicating success of the conversion. The converted value, on success, is returned in an out parameter.

Upvotes: 5

Related Questions