Необходимо ли назначать значение по умолчанию для варианта, возвращаемого из функции Delphi? - PullRequest
7 голосов
/ 08 июля 2011

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

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 был FALSE, компилятор создал его, но он был «Unassigned» (<> nil?).Чтобы еще больше поощрять это мышление, компилятор не выдает предупреждения, если вы пропустите присвоение Result.

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

Должен ли я ВСЕГДА назначать Result (как я это делаю при использовании предопределенных типов) при получении варианта?

Ответы [ 2 ]

9 голосов
/ 08 июля 2011

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

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

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

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

Во-первых, вот доказательство того, что функция, возвращающая 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» глобальной переменной и вызовем функцию, возвращающую Variant, мы получим этот код:

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, является allways , инициализированный как Unassigned byвызывающая сторона.Не правда.Если в предыдущем тесте мы сделали переменную «X» локальной переменной (не глобальной), компилятор больше не использует две отдельные локальные временные переменные.Таким образом, у нас есть два отдельных случая, когда компилятор генерирует другой код.Другими словами, не делайте никаких предположений, всегда присваивайте Result.

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

4 голосов
/ 08 июля 2011

Должен ли я ВСЕГДА назначать Result (как, например, при использовании предопределенных типов) при получении варианта?

Да.

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

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.

...