Необходимо ли присваивать значение по умолчанию варианту, возвращаемому из функции Delphi?


Постепенно я стал использовать больше вариантов-они могут быть очень полезны в определенных местах для переноса типов данных, которые не известны во время компиляции. Одно полезное значение не назначено ("у меня нет значения для вас"). Мне кажется, я давно обнаружил, что функция:

function DoSomething : variant;
begin
  If SomeBoolean then
    Result := 4.5
end;

, по-видимому, эквивалентно:

function DoSomething : variant;
begin 
  If SomeBoolean then
    Result := 4.5
   else
   Result := Unassigned; // <<<<
end;

Я предположил, что вариант должен быть создан динамически, и если SomeBoolean был ложным, компилятор создал его, но он был "Неназначен" ( ноль?). Чтобы еще больше стимулировать это мышление, компилятор не сообщает о предупреждении, если вы опускаете присвоение результата.

Только что я заметил неприятную ошибку, когда мой первый пример (где 'Result' не является эксплицитным значением по умолчанию 'nil') фактически вернул "старое" значение откуда-то еще.

Должен ли я всегда назначать результат (как я делаю при использовании префиксных типов) при повторном выборе варианта?

2 6

2 ответа:

Да, вам всегда нужно инициализироватьResult функции , даже если это управляемый тип (например, string и Variant). Компилятор действительно генерирует некоторый код для инициализации будущего возвращаемого значения функции Variant для вас (по крайней мере, компилятор Delphi 2010, который я использовал для тестирования, делает это), но компилятор не гарантирует, что ваш результат будет инициализирован; это только усложняет тестирование, потому что вы можете столкнуться с ситуацией, когда ваш результат был инициализирован, основывая свои решения на это только для того, чтобы позже обнаружить, что ваш код глючит, потому что при определенных обстоятельствах результат не был инициализирован.

Из моего исследования я заметил:

  • Если ваш результат присваивается глобальной переменной, ваша функция вызывается с инициализированной скрытой временной переменной, создавая иллюзию, что результат магически инициализирован.
  • Если вы сделаете два назначения одной и той же глобальной переменной, вы получите два различных скрытых временных переменные, усиливающие иллюзию того, что результат инициализирован.
  • Если вы делаете два назначения одной и той же глобальной переменной, но не используете глобальную переменную между вызовами, компилятор использует только 1 скрытое временное, тормозящее предыдущее правило!
  • Если ваша переменная является локальной для вызывающей процедуры, то никакая промежуточная скрытая локальная переменная не используется вообще, поэтому результат не инициализируется.

Демонстрация:

Во-первых, вот доказательство того, что a функция, возвращающая Variant, получает скрытый параметр var Result: Variant. Следующие два компилируются на один и тот же ассемблер, как показано ниже:

procedure RetVarProc(var V:Variant);
begin
  V := 1;
end;

function RetVarFunc: Variant;
begin
  Result := 1;
end;

// Generated assembler:
push ebx // needs to be saved
mov ebx, eax // EAX contains the address of the return Variant, copies that to EBX
mov eax, ebx // ... not a very smart compiler
mov edx, $00000001
mov cl, $01
call @VarFromInt
pop ebx
ret

Далее, интересно посмотреть, как вызов для этих двух устанавливается исполнителем. Вот что происходит при вызове процедуры, имеющей параметр var X:Variant:

procedure Test;
var X: Variant;
begin
  ProcThatTakesOneVarParameter(X);
end;

// compiles to:
lea eax, [ebp - $10]; // EAX gets the address of the local variable X
call ProcThatTakesOneVarParameter

Если мы сделаем эту" X " глобальной переменной и вызовем функцию, возвращающую вариант, мы получим следующий код:

var X: Variant;

procedure Test;
begin
  X := FuncReturningVar;
end;

// compiles to:
lea eax, [ebp-$10] // EAX gets the address of a HIDDEN local variable.
call FuncReturningVar // Calls our function with the local variable as parameter
lea edx, [ebp-$10] // EDX gets the address of the same HIDDEN local variable.
mov eax, $00123445 // EAX is loaded with the address of the global variable X
call @VarCopy // This moves the result of FuncReturningVar into the global variable X

Если вы посмотрите на пролог этой функции вы заметите, что локальная переменная, используемая в качестве временного параметра для вызова FuncReturningVar, инициализируется нулем. Если функция не содержит операторов Result :=, то X будет "Неинициализирована". При повторном вызове функции используется другая временная и скрытая переменная! Вот немного примерного кода, чтобы увидеть это:

var X: Variant; // global variable
procedure Test;
begin
  X := FuncReturningVar;
  WriteLn(X); // Make sure we use "X"
  X := FuncReturningVar;
  WriteLn(X); // Again, make sure we use "X"
end;

// compiles to:
lea eax, [ebp-$10] // first local temporary
call FuncReturningVar
lea edx, [ebp-$10]
mov eax, $00123456
call @VarCopy
// [call to WriteLn using the actual address of X removed]
lea eax, [ebp-$20] // a DIFFERENT local temporary, again, initialized to Unassigned
call FuncReturningVar
// [ same as before, removed for brevity ]

Глядя на этот код, можно подумать, что "результат" функции, возвращающей Variant, всегдаинициализируется в Unassigned вызовом вечеринка. Неправда. Если в предыдущем тесте мы сделаем переменную "X" локальной (а не глобальной), компилятор больше не будет использовать две отдельные локальные временные переменные. Таким образом, мы имеем два отдельных случая, когда компилятор генерирует различный код. Другими словами, Не делайте никаких предположений, всегда назначайте Result.

Мое предположение о другом поведении: если переменная Variant может быть доступна вне текущей области, как глобальная переменная (или поле класса, если на то пошло), то компилятор генерирует код, который использует потокобезопасную функцию @ VarCopy. Если переменная локальна для функции, нет проблем с многопоточностью, поэтому компилятор может взять на себя смелость выполнять прямые назначения (больше не вызывая @VarCopy).

Должен ли я всегда назначать результат (как я это делаю при использовании предопределенных типов), когда повторный вариант?

Да.

Проверьте это:

function DoSomething(SomeBoolean: Boolean) : variant;
begin
    if SomeBoolean then
        Result := 1
end;

Используйте функцию следующим образом:

var
    xx: Variant;
begin
    xx := DoSomething(True);
    if xx <> Unassigned then
        ShowMessage('Assigned');

    xx := DoSomething(False);
    if xx <> Unassigned then
        ShowMessage('Assigned');
end;

Xx все равно будет назначен после второго вызова DoSomething.

Измените функцию следующим образом:

function DoSomething(SomeBoolean: Boolean) : variant;
begin
    Result := Unassigned;
    if SomeBoolean then
        Result := 1
end;

И xx не назначается после второго вызова DoSomething.