Mastering Delphi Programming:A Complete Reference Guide
上QQ阅读APP看书,第一时间看更新

Parameter passing

In essence, Delphi knows two ways of passing parameters to a method (or procedure, or function, or anonymous method, it's all the same). Parameters can be passed by value or by reference.

The former makes a copy of the original value and passes that copy to a method. The code inside the method can then modify its copy however it wants and this won't change the original value.

The latter approach doesn't pass the value to the method but just an address of that value (a pointer to it). This can be faster than passing by value as a pointer will generally be smaller than an array or a record. In this case, the method can then use this address to access (and modify) the original value; something that is not possible if we pass a parameter by value.

Passing by reference is indicated by prefixing a parameter name with var, out, or const. A parameter is passed by value when no such prefix is used.

The difference between var and out is mostly semantic (although small changes in generated code are possible). Prefixing a parameter by out tells the compiler that we are not passing anything into the method (it may even be an uninitialized value) and we are only interested in the result. The var prefix, on the other hand, declares that we will be passing some value in and that we expect to get something out.

In practice, var is usually used even when a parameter is not used to provide any data to the method; just to return something.

The const prefix is a bit different. When it is used, the compiler will prevent us from making any changes to the parameter. (With some exceptions, as we'll soon see.) In essence, const is an optimization prefix used when we want to pass a larger amount of data (a record, for example). It will ensure that the parameter is passed by reference while the compiler will make sure that the data is not changed.

The const prefix is also useful when the parameter is a managed type (for example a string or interface). When you pass such type by value, the code increments the reference count at the beginning of the method and decrements it at the end. If the parameter is marked as const, the reference count is not incremented/decremented and this can make a difference in the long run.

The ParameterPassing demo passes an array, string, record, and interface parameters to a method multiple times and measures the time. I will skip most of the code here as it is fairly dull and looks all the same. I'll just focus on a few interesting details first and then give an overview of the measured results.

The most uninteresting of all are static arrays and records. Passing by value makes a copy while passing by reference just passes a pointer to the data. That is all.

When you pass a string by using const, just an address is passed to the method and a reference count is not touched. That makes such calls very fast.

Passing by value, however, uses the copy-on-write mechanism. When a string is passed by value, a reference count is incremented and a pointer to string data is passed to the method. Only when the method modifies the string parameter,  is a real copy of the string data made.

As an example, the following code in the demo is called 10 million times when you click the string button:

procedure TfrmParamPassing.ProcString(s: string);
begin
// Enable next line and code will suddenly become much slower!
// s[1] := 'a';
end;

This executes in a few hundred milliseconds. Uncomment the assignment and the code will suddenly run for 20 seconds (give or take) as a copy of quite large string will be made each time the method is called.

Interfaces behave similarly to strings, except that there is no copy-on-write mechanism. The only difference in execution speed comes from incrementing and decrementing the reference count.

At the beginning of this section, I promised some interesting results, so here they are! When a parameter is a dynamic array, strange things happen. But let's start with the code.

To measure parameter passing, the code creates an array of 100,000 elements, sets the element [1] to 1 and calls ScanDynArray 10,000 times. ScanDynArray sets arr[1] to 42 and exits.

Another method, btnConstDynArrayClick (not shown) works exactly the same except that it calls ScanDynArrayConst instead of ScanDynArray:

const
CArraySize = 100000;

procedure TfrmParamPassing.ScanDynArray(arr: TArray<Integer>);
begin
arr[1] := 42;
end;

procedure TfrmParamPassing.ScanDynArrayConst(const arr: TArray<Integer>);
begin
// strangely, compiler allows that
arr[1] := 42;
end;

procedure TfrmParamPassing.btnDynArrayClick(Sender: TObject);
var
arr: TArray<Integer>;
sw: TStopwatch;
i: Integer;
begin
SetLength(arr, CArraySize);
arr[1] := 1;
sw := TStopwatch.StartNew;
for i := 1 to 10000 do
ScanDynArray(arr);
sw.Stop;
ListBox1.Items.Add(Format('TArray<Integer>: %d ms, arr[1] = %d',
[sw.ElapsedMilliseconds, arr[1]]));
end;

Can you spot the weird code in this example? It is the assignment in the ScanDynArrayConst method. I said that the const prefix will prevent such modifications of parameters, and in most cases it does. Dynamic array are, however different.

When you pass a dynamic array to a method, Delphi treats it just like a pointer. If you mark it as const, nothing changes as only this pointer is treated as a constant, not the data it points to. That's why you can modify the original array even through the const parameter.

That's the first level of weirdness. Let's crank it up!

If you check the System unit, you'll see that TArray<T> is defined like this:

type
TArray<T> = array of T;

So, in theory, the code will work the same if we replace TArray<Integer> with array of Integer? Not at all!

procedure TfrmParamPassing.ScanDynArray2(arr: array of integer);
begin
arr[1] := 42;
end;

procedure TfrmParamPassing.ScanDynArrayConst2(const arr: array of integer);
begin
// in this case following line doesn't compile
// arr[1] := 42;
end;

In this case, the compiler won't allow us to modify arr[1] when the parameter is marked const! Even more, in the ScanDynArray2 method, the code makes a full copy of the array, just as if we were passing a normal static array!

This is a legacy from the versions before Delphi 2009 when we didn't have generics yet but we already got dynamic arrays through the array of T syntax. The compiler has special handling for this syntax built-in and this handling is still in effect in the modern-day.

Is this weird enough for you? No? No problem, I can go one better.

If we declare type TIntArray = array of Integer and then rewrite the code to use this array, we get the original TArray<T> behavior back. The array is always passed by reference and the code can modify the original array through the const parameter:

type
TIntArray = array of Integer;

procedure TfrmParamPassing.ScanDynArray3(arr: TIntArray);
begin
arr[1] := 42;
end;

procedure TfrmParamPassing.ScanDynArrayConst3(const arr: TIntArray);
begin
// it compiles!
arr[1] := 42;
end;

Let's now analyze the results of the ParameterPassing demo program.

Static arrays are passed as a copy (by value) or as a pointer (by reference). The difference in speed clearly proves that (174 vs. 0 ms). In both cases, the original value of the array element is not modified (it is 1).

The same happens when the array is declared as array of Integer (lines starting with array of Integer and const array of Integer) in the image.

TArray<Integer> and TIntArray behave exactly the same. An array is always passed by reference and the original value can be modified (it shows as 42 in the log).

Records are either copied (by value) or passed as a pointer (by reference) which brings the difference of 157 vs. 51 ms.

Strings are always passed as a pointer. When they are passed as a normal parameter, the reference count is incremented/decremented. When they are passed as a const parameter, the reference count is not modified. This brings a difference of 257 vs. 47 ms. A similar effect shows with the interface type parameters:

I have to point out that demos for different types cannot be compared directly with one another. For example, the loop for testing array types repeats 10,000 times while the loop for testing strings and interfaces repeats 10,000,000 times.