Result
в тесте НЕ всегда изначально ()
Я нашел Нужно ли устанавливать длину динамического массива при инициализации?, однако я не совсем понимаю этот ответ
Что еще более важно, как лучше всего «разорвать» соединение Result/A (мне нужен цикл)? Возможно, какой-то способ заставить компилятор "правильно" инициализироваться? Вручную добавить Result := nil
в качестве первой строки в тесте?
function Test(var A: TArray<Integer>): TArray<Integer>;
begin
SetLength(Result, 3); // Breakpoint here
Result[0] := 2;
end;
procedure TForm1.Button3Click(Sender: TObject);
var
A: TArray<Integer>; // Does not help whether A is local or global
I: Integer;
begin
for I := 1 to 3 do
A := Test(A); // At Test breakpoint:
// * FIRST loop: Result is ()
// * NEXT loops: Result is (2, 0, 0)
// modifying Result changes A (immediately)
A := Test(A); // Result is again ()
end;
Маркодор: это всего лишь MRE, не ожидайте, что это будет иметь слишком много смысла; несмотря на это, начиная с цикла for #2, Result IS инициализируется в точке останова, см. комментарии в коде
Нет, он только кажется инициализированным. Он просто повторно использовал один и тот же адрес памяти, без гарантии, что это всегда будет происходить. Вы всегда должны инициализировать локальные переменные. См.: docwiki.embarcadero.com/RADStudio/Sydney/en/Variables_(Delphi)
Маркодор: не согласно stackoverflow.com/questions/5314918/…: «Динамические массивы являются управляемыми типами и поэтому всегда инициализируются нулем»; исключение: «компилятор в качестве оптимизации выбирает не повторно инициализировать неявную локальную переменную внутри цикла»
Да, управляемые типы инициализируются, но не возвращаемые значения Result/function.
Я не понимаю вашей проблемы. В первом цикле A пусто до выполнения теста и [2,0,0] после выполнения. var A
причины modifying Result changes A (immediately)
MBo: конечно, A будет присвоен результат ПОСЛЕ теста, но НЕ сразу очевидно, что изменение Result в точке останова НЕМЕДЛЕННО повлияет на него
Маркодор: насколько я понимаю (что может быть неправильным, отсюда и этот вопрос), управляемые типы результатов, такие как целые числа, динамические массивы и т. д., ЯВЛЯЮТСЯ «правильно» инициализированными; это крайний случай, когда этого не происходит из-за того, что он находится внутри цикла for (обратите внимание, что последний тестовый вызов после цикла ведет себя «как и ожидалось»)
Результат никогда не инициализируется. Integer не является управляемым типом.
Это предполагаемое поведение, оптимизация производительности. Есть несколько статей об этом, которые я попытаюсь найти.
Я думаю, что мы, вероятно, могли бы закрыть этот вопрос как дубликат того, на который я ссылался.
Дэвид Хеффернан: на самом деле я упомянул об этом в исходном вопросе. я до сих пор не понимаю, можно ли отключить эту оптимизацию или как лучше всего решить/избежать эту проблему
Упомянутый вопрос касается полей внутри класса, и все они инициализированы нулями, а управляемые типы должным образом завершаются во время уничтожения экземпляра.
Ваш код предназначен для вызова функции с управляемым типом возврата в цикле. Локальная переменная управляемого типа инициализируется один раз - в начале подпрограммы. Управляемый возвращаемый тип под капотом обрабатывается компилятором как параметр var. Таким образом, после первого вызова он дважды передает то, что выглядит как A
, в Test
— как параметр A
и для Result
.
Но ваша оценка того, что изменение Result
также влияет на A
(параметр), неверна, что мы можем доказать, немного изменив код:
function Test(var A: TArray<Integer>; I: Integer): TArray<Integer>;
begin
SetLength(Result, 3); // Breakpoint here
Result[0] := I;
end;
procedure Main;
var
A: TArray<Integer>;
I: Integer;
begin
for I := 1 to 3 do
A := Test(A, I);
A := Test(A, 0);
end;
Когда вы сделаете один шаг через Test
, вы увидите, что изменение Result[0]
не изменит A
. Это потому, что SetLength
создаст копию, потому что компилятор ввел вторую переменную, которую он временно использует для передачи результата, и после вызова Test
он присваивает ее A
(локальной переменной) — вы можете видеть это в представлении дизассемблирования, которое будет выглядеть аналогично к этому для строки в цикле (я использую $O+, чтобы сделать код немного плотнее, чем он был бы без оптимизации):
Project1.dpr.21: A := Test(A, I);
0041A3BD 8D4DF8 lea ecx,[ebp-$08]
0041A3C0 8D45FC lea eax,
0041A3C3 8BD3 mov edx,ebx
0041A3C5 E8B2FFFFFF call Test
0041A3CA 8B55F8 mov edx,[ebp-$08]
0041A3CD 8D45FC lea eax,[ebp-$04]
0041A3D0 8B0DC8244000 mov ecx,[$004024c8]
0041A3D6 E855E7FEFF call @DynArrayAsg
0041A3DB 43 inc ebx
Зная соглашение о вызовах по умолчанию — это первые три параметра в eax, edx и ecx, мы знаем, что eax — это параметр A
, edx — это I
, а ecx
— это Result
(вышеупомянутый параметр Result var всегда является последним). Мы видим, что он использует разные места в стеке ([ebp-$04]
это переменная A
и [ebp-$08]
переменная, введенная компилятором). И после вызова мы видим, что компилятор вставил дополнительный вызов System._DynArrayAsg
, который затем назначает введенную компилятором временную переменную для Result
на A
.
Вот скриншот второго вызова Test
:
ваш анализ неверен: согласно отладчику (и то, что ОНА говорит, идет! ;) модификация Result
в точке останова НЕМЕДЛЕННО модифицирует A
-- начиная со 2-го цикла; это, что неудивительно, также происходит с вашим измененным кодом
@hundreAd, не могли бы вы еще раз медленно прочитать ответ?
Стефан Глиенке: нельзя публиковать скриншоты в комментариях (афаик?), но, тем не менее, ваша точка останова другая: она ПОСЛЕ SetLength
, посмотрите, что происходит ДО ее выполнения
Стефан Глиенке: это может быть случай, когда MRE слишком прост, чтобы представить сложность фактического кода, мне придется продолжить расследование, чтобы увидеть, действительно ли существует случай, когда A «на месте» перезаписывается изменениями в Result. (процедура генерирует результат на основе A и, наконец, присваивает результат A, но A должен оставаться неизменным в течение этого процесса)
Стефан Глиенке: попробуйте добавить в качестве первой строки в тесте: if Length(Result)>0 then Result[0] := 66;
Хотя я не решаюсь назвать эту For
оптимизацию компилятора ошибкой, это, безусловно, бесполезно при непосредственном изменении элементов массива:
function Test(var A: TArray<Integer>): TArray<Integer>;
begin
if Length(Result) > 0 then // Breakpoint
Result[1] := 66; // A modified!
SetLength(Result, 3);
Result[0] := Result[0] + 1; // A not modified
Exit;
A[9] := 666; // Force linker not to eliminate A
end;
После исследования я пришел к выводу, что функции, влияющие на весь массив (например, SetLength
, Copy
или какая-то другая функция, возвращающая TArray<Integer>
), неудивительно, что «нарушат» идентичность Результат/А, созданную циклом For
.
Казалось бы, самый безопасный подход (согласно ответу, указанному в исходном посте) — Result := nil;
в качестве первой строки в тесте.
Если нет дальнейших предложений, я в конечном итоге приму это как ответ.
ПРИМЕЧАНИЕ:
В качестве дополнительного бонуса, начиная с Result := nil
, массив не будет скопирован с помощью SetLength
— очевидно, но, например, для массив из 100000 зацикливается 100000 раз, эта небольшая модификация приводит к сокращению времени выполнения примерно на 40%
Я уже объяснил в своем ответе, что SetLength
создает копию - конечно, она изменит A
, если вы напишете Result
перед вызовом SetLength
, потому что динамические массивы не имеют копирования при записи, как строки. Кроме того, это должно быть изменением вашего вопроса, а не ответом.
Стефан Глиенке: не модификация, а просто уточнение - похоже, вы не изменили значения (в точке останова) через отладчик. несмотря на это, это вызвано тем, что цикл for не повторно инициализирует Result, иначе код вел бы себя так, как [i, по крайней мере] ожидалось; ответ предназначен для того, чтобы решить эту проблему, чего не сделал ваш пост
Я никогда не понимал исходный комментарий modifying Result changes A (immediately)
как «изменение некоторого значения в Результате с помощью отладчика» - поэтому в вашем вопросе отсутствует информация, которую знаете только вы.
Стефан Глиенке: я (очевидно, ошибочно) полагал, что все ясно: значение Result[?] должно быть изменено до выполнения инструкции в точке останова. делаете ли вы это с помощью кода, с помощью отладчика или любым другим способом... я исправлен и постараюсь быть еще более ясным в следующий раз; что касается комментария immediately
- не очень очевидно, что изменение Result в точке останова также изменит A (после завершения функции Result, конечно, будет присвоен A)
В вашей точке останова массив Result еще не инициализирован. Почему вы передаете «A» в качестве аргумента var в Test( если вы его не изменяете? В вашем случае, делая A := Test(A); это бессмыслица. Содержимое локальной переменной не определено, пока им не будет присвоено значение .