Thursday, June 30, 2011

Backward Compatibility using Defalt Parameters

When a function, procedure, or method requires at least one parameter, the final (right-most) parameters in the parameter list can be declared to have a default value. When the parameters declared to have a default value are omitted during the invocation of the subroutine, the default values are assigned to the corresponding formal parameters.

While this feature is interesting by itself, there is another use for this feature that permits you to extend an existing method without breaking older versions.

The best way to explain this trick is with an example. Imaging that you have a method you call to display an error message on your page. This method takes a single string parameter, and assigns that value to one or more Label controls (or some other display element).

Here is an example of what the implementation of that method might look like:

procedure TForm1.SetMessageText(Value: String);
begin
  MessageLabel1.Caption := Value;
  MessageLabel2.Caption := Value;
end;

Let's assume that these Label controls use a red foreground color, so that when a non-empty string is assigned the message stands out, and looks like an error message. Assigning an empty string to the method sets the Labels's caption property to an empty string, effectively turning off the message.

So, whenever you need to display an error message, you simply call this method, using something similar to the following:

SetMessageText('You must enter a starting date before continuing');

Now image that at some later time you realize that you want to be able to display non-error messages using these same Labels. These non-error messages should use a black font for the Labels, and not a red one.

Instead of create a new method, you can simply extend the existing method easily by adding a new parameter. Importantly, in order to avoid having to go back and modify your existing calls to SetMessageText, this second parameter will use a default value to direct the method to use a red font when this parameter is absent.

Here is how the updated method implementation might look:

procedure TForm1.SetMessageText(Value: String; IsError: Boolean = True);
begin
  if IsError then
  begin
    MessageLabel1.Font.Color := clRed;
    MessageLabel2.Font.Color := clRed;
  end
  else
  begin
    MessageLabel1.Font.Color := clBlack; 
    MessageLabel2.Font.Color := clBlack;
  end;
  MessageLabel1.Caption := Value;
  MessageLabel2.Caption := Value;
end;
Now, when you use a call something like the following, your message will be display in a black font, indicating that this is an informational message:

SetMessageText('The time is ' + DateTimeToStr(Now), False);

Any call that omits the second parameter, or calls that include True in the second parameter, will display the message in a red font.

There are several restrictions concerning the use of default parameters. These are:
  • The specified default values must be represented by a constant expression
  • Parameters that are object types, variants, records, or static arrays cannot have default values
  • Parameters that are class, class reference, dynamic arrays, and procedural types can only have nil as their default value

5 comments:

  1. Very bad idea.

    This only works if you recompile all the code using this. As soon as you have this kind of method as export in some dll or package this breaks your code since the compiler actually provided the default value for the call.

    For example a host application that was compiled with the first version (without default parameter) will get an AV when calling the new version of this method. So you have to change the signature for the import anyway.

    Another bad thing with default parameters is when you change the default value. In that case you also have to recompile everything that uses the changed method.

    If I remember .Net ran into the same problem when they introduced that feature. People compiled assemblies using routines with default parameters. Then they changed the default values and wondered why they got called with the wrong values. Because the depending assemblies were not compiled after changing the default value.

    ReplyDelete
  2. I am wondering... what does the "MessageLabel2.Caption" statement do? ;)

    ReplyDelete
    Replies
    1. Oh, something is missing. Should be 'MessageLabel2.Caption = Value;'. Fixed.

      Delete
  3. I disagree that this is a bad idea. However, you make a good point, but as you mentioned, your complaint only applies when the method and the code that calls it are located in different executables. If they are compiled within the same project, even when the modified method lies in a unit shared by two or more projects, it is safe. Those other projects that use the shared unit will not be affected initially, since they have the correct version linked into their executable. If they are later modified, they will see and can use the new code. This makes this a very useful technique under the right circumstances.

    ReplyDelete
  4. I just wanted to say that Stefan's comment on this page ended a half-day-long quest to determine why my default parameter was passing the wrong value. I just needed to hit "build" instead of "compile" to get my new default value to propagate. Thanks, Stefan - and thanks, Cary, for producing the conversation in the first place! :-)

    ReplyDelete