код работает только с промежуточной переменной

Приветствую!

Использую рекурсию. Получилось так, что при рекурсивном вызове функции поместить полученное значение в массив (запись) получается только через промежуточную переменную. Как такое возможно и в чем может быть причина?

imlpementation

mFolderData = record
sizefolder: int64;
fullpath: string;

var
  mFoldersList: array of mFolderData;

function Form1.ScanCurrentFolder(mPath: string): int64;
begin

// так НЕ работает
mFoldersList[i].sizefolder := ScanCurrentFolder(mFoldersList[i].fullpath);
// а вот так работает
var mTmp: int64 := ScanCurrentFolder(mFoldersList[i].fullpath);
mFoldersList[i].sizefolder := mTmp;

end;

Используйте Markdown для выделения кода. И залейте код целиком, так чтобы можно было запустить и увидеть в дебагере разницу поведения. Для этого желательно найти минимум кода, с которым проблема будет воспроизводится.

Благодарю за подсказку на счет Markdown, я раньше с ним не сталкивался.

По поводу кода это проблемно, поскольку функция достаточно большая. Интересно само явление использования промежуточной переменной - обычно компилятор сам оптимизирует такой код (по крайней мере должен), а тут без нее не работает. Поскольку отладчика у меня нет, проблема выяснилась через MessageBox.

// НЕрабочий вариант
mFoldersList[i].sizefolder := 0;
mFoldersList[i].sizefolder := ScanCurrentFolder(mFoldersList[i].fullpath);
MessageBox(Handle.ToInt32, mFoldersList[i].sizefolder, 'Warning', 0); // отображает 0

// рабочий вариант
mFoldersList[i].sizefolder := 0;
var mTmp: int64 := ScanCurrentFolder(mFoldersList[mCurrentFolderIndex].fullpath);
mFoldersList[i].sizefolder := mTmp;
MessageBox(Handle.ToInt32, mFoldersList[i].sizefolder, 'Warning', 0); // отображает размер папки

В тему углубляться не буду, поскольку проблема решилась. Но хотелось бы понять, почему именно это происходит, может есть какие-то ограничения или недоработки .NET или самого паскаля abc о которых мне неизвестно. Если такой информации нет, то тему можно закрыть, возможно такое решение кому-то поможет в будущем

Поэтому я сказал про MRE.
Делаете копию своего проекта и начинаете удалять куски кода, так чтобы проблема оставалась.

Ещё, вместо MessageBox (который в вашем WinForms приложении можно было бы и без external вызывать, используя класс с тем же именем) - лучше использовать дебагер.
Тыкните на панель слева от кода - на эту строчку поставить красную точку останова. Когда выполнение дойдёт до неё - код станет на паузу, а вы сможете наводить мышку на имена в коде чтобы смотреть что там происходит. А снизу появится ещё несколько вкладок, как просмотр выражений.

Отдельная переменная это костыль а не решение. И она не должна ничего менять, потому что в готовом .exe файле всё равно используется промежуточное хранилище для результата вызова функции. Тут скорее меняется что-то ещё, вместе с доп. переменной - это надо поймать, чтобы исправить.

Сейчас основная проблема в том что я не могу воспроизвести. У меня во всех случаях значение меняется.

type
  r1 = record
    i: int64;
    s: string;
    
    procedure p1;
    begin
      i := 5;
      s := 'def';
    end;
    
  end;
  
var a := new r1[5];
function f1(i: int64): int64;
begin
  Result := 123;
  if i=0 then exit;
  a[i].i := f1(i-1);
end;

begin
  f1(1);
  a[2].i := 3;
  a[2].s := 'abc';
  a[3].p1;
  a.PrintLines;
end.

Останов (красную точку) я умею ставить и смотреть переменные наведением курсора :slight_smile:

Попробую сделать MRE.

1 лайк

Максимально сократил код. На форме только кнопка и диалог выбора папки. Для проверки один из вариантов (рабочий/нерабочий) необходимо закомментировать. MessageBox показывает размер папки.

Функция запускается в потоке, но и без потока та же ситуация.

unit Unit1;

interface

uses
  System, System.Threading, System.IO, System.Windows.Forms;

type
  Form1 = class(Form)
    procedure button1_Click(sender: Object; e: EventArgs);
    procedure GetFirstThread;
  {$region FormDesigner}
  internal
    {$resource Unit1.Form1.resources}
    button1: Button;
    folderBrowserDialog1: FolderBrowserDialog;
    {$include Unit1.Form1.inc}
  {$endregion FormDesigner}
  public
    constructor;
    begin
      InitializeComponent;
    end;
  end;

implementation

type
  mFileData = record // запись Файл
    sizefile: int64; // размер файла
  end;
  
  mFolderData = record // запись Папка
    sizefolder: int64; // размер папки
    fullpath: string; // полный путь к папке
  end;

var
  mThreadScan: Thread;                // поток сканирования начальной папки
  mCountAllFiles: int64;              // количество всех файлов
  mCountAllFolders: int64;            // количество всех папок
  mMainFolder: string = '';           // начальная папка
  mFilesList: array of mFileData;     // массив данных всех файлов
  mFoldersList: array of mFolderData; // массив данных всех папок

function MessageBox(h: integer; m, c: string; t: integer): integer; external 'User32.dll' name 'MessageBox';

function ScanCurrentFolder(mPath: string): int64;
begin
  var mSum: int64 := 0;
  var mFolders := System.IO.Directory.GetDirectories(mPath);
  var mFiles := System.IO.Directory.GetFiles(mPath);
  if mFolders.Count > 0 then
    for var i: int64 := 0 to (mFolders.Count - 1) do
    begin
      inc(mCountAllFolders);
      SetLength(mFoldersList, mCountAllFolders);
      mFoldersList[mCountAllFolders - 1].fullpath := '';
      mFoldersList[mCountAllFolders - 1].sizefolder := 0;
      var mFolderInfo := new DirectoryInfo(mFolders[i]);
      mFoldersList[mCountAllFolders - 1].fullpath := mFolderInfo.FullName;
      //
      // == начало НЕрабочего варианта ==
      mFoldersList[mCountAllFolders - 1].sizefolder := ScanCurrentFolder(mFoldersList[mCountAllFolders - 1].fullpath);
      // == конец НЕрабочего варианта ==
      //
      // == начало рабочего варианта ==
      var mTmp: int64 := ScanCurrentFolder(mFoldersList[mCountAllFolders - 1].fullpath);
      mFoldersList[mCountAllFolders - 1].sizefolder := mTmp;
      // == конец рабочего варианта ==
      //
      mSum += mFoldersList[mCountAllFolders - 1].sizefolder;
    end;
  if mFiles.Count > 0 then
    for var j: int64 := 0 to (mFiles.Count - 1) do
    begin
      inc(mCountAllFiles);
      SetLength(mFilesList, mCountAllFiles);        
      mFilesList[mCountAllFiles - 1].sizefile := 0;
      var mFileInfo := new FileInfo(mFiles[j]);
      mFilesList[mCountAllFiles - 1].sizefile := mFileInfo.Length;
      mSum += mFilesList[mCountAllFiles - 1].sizefile;
    end;
  Result := mSum;
end;

procedure Form1.GetFirstThread;
begin
  mCountAllFiles := 0;
  mCountAllFolders := 0;
  SetLength(mFilesList, 0);
  SetLength(mFoldersList, 0);
  var mSizeMainFolder: int64 := ScanCurrentFolder(mMainFolder);
  //
  MessageBox(Handle.ToInt32, mSizeMainFolder.ToString, 'Folder Size', 0);
  //
end;

procedure Form1.button1_Click(sender: Object; e: EventArgs);
begin
  folderBrowserDialog1.ShowDialog;
  mMainFolder := folderBrowserDialog1.SelectedPath;
  if mMainFolder <> '' then
  begin
    mThreadScan := new Thread(GetFirstThread);
    mThreadScan.Start;
  end;  
end;

end.

Это далеко от минимального кода… Ну, вы хоть архивом киньте, этот 1 файл запустить не выйдет потому что он является только частью проекта.

Меньше потеряется смысл вычисления, который я могу проверить.

Это весь код unit1.pas для формы с одной кнопкой и компонентом открытия папки. Полагаю, воспроизвести несложно

скачать архив с проектом

Если воспроизвести - будет не точный тест, потому что всё равно у меня будет не тот же код в $include-ах и т.п.

прикрепил ссылку к предыдущему сообщению

Всё считает как ожидалось.
Надо таки на вашей стороне абсолютно минимальный код найти, чтобы понять что у вас не так как у меня работает.
Только, для начала - у вас версия паскаля последняя стоит? Скачать

Так а зачем вам изначальный смысл сохранять? Надо найти минимальный код который даёт неожидаемое поведение, а не тот - который считает то же самое, что вы пытались посчитать в оригинальной программе.

В первую очередь надо вообще от WinForms избавится. Сделать, чтобы в отдельной консольной программе, состоящей из 1 файла, такое же поведение получалось.
Тогда поиск минимального кода на много быстрее пойдёт. Не надо будет при каждом запуске выбирать папку, для которой считать - запускаете и сразу смотрите, ожидаемое-ли кол-во байт вывело.

И саму папку для которой считаете - стоит минимизировать. Я так понимаю, нужно только чтобы в ней была 1 подпапка с 1 файлом - тогда проблемная строчка будет выполняться только 1 раз.

На самом деле вы могли и на форум залить… Тянете файл-архив и кидаете прямо в окно ответа в браузере - он сразу прикрепится.

Версия 3.9 сборка 3340 от 12.08.2023

В консольной версии проблема осталась.

Ссылка

На форум не получается залить:

Мда, покопался в сгенерированных .exe, настроил себе ещё удобства для тестирования, к примеру:

for check := false to true do

Чтобы автоматически переключалось между режимом с- и без ошибки.

В итоге понял что проблема только в “Новая папка (4)” - всё остальное можно поудалять.

И файл проекта непонятно зачем вообще, тут и без него работает.
А строчка program только декомпилировать мешает - часть имён в project1 лежат, а часть в Project1.


И только после всего этого понял, что вы на каждой итерации создаёте новый массив и копируете в него всё что было в mFilesList.
Если всё на 1 строчке - ссылку на mFoldersList[mCountAllFolders - 1].sizefolder (чтобы знать куда присвоить) берёт до вызова, в котором память пере-выеделяется. И после перевыделения памяти эта ссылка продолжает ссылаться на старый массив.

Вообще, экземпляр mFolderData неплохо было бы на стеке (в локальной переменной) заполнять, перед тем как в массив пихать. Иначе вы делаете кучу дорогостоющих обращений в RAM.
Или ещё лучше - опишите конструктор в записи и вызывайте его чтобы создать экземпляр целиком, вместо отдельный обращений к полям. И заодно код значительно чище выглядеть будет.

А вместо SetLength на каждой итерации - лучше бы каждый раз как заканчивается место - увеличивать размер, к примеру, в 2 раза.
И уже существует готовый тип данный, который делал бы это за вас - List<mFolderData>.
Правда, из за того что он оборачивает массив - записать отдельно 1 поле записи по индексу не выйдет. Но опять же, если использовать конструктор - в этом нет необходимости.

uses System.IO;

type
  mFileData = record // запись Файл
    sizefile: int64; // размер файла
    
    constructor(sizefile: int64) :=
      self.sizefile := sizefile;
    
  end;
  
  mFolderData = record // запись Папка
    fullpath: string; // полный путь к папке
    sizefolder: int64; // размер папки
    
    constructor(fullpath: string; sizefolder: int64) :=
      (self.fullpath, self.sizefolder) := (fullpath, sizefolder);
    
  end;
  
var all_scanned_files := new List<mFileData>;
var all_scanned_folders := new List<mFolderData>;
function ScanCurrentFolder(mPath: string): int64;
begin
  Result := 0;
  
  // GetDirectories это EnumerateDirectories(...).ToArray
  // Система всё равно их по 1 перечисляет, вместо того чтобы всё вместе возвращать
  // Поэтому преобразование к массиву только всё замедляет
  foreach var sub_path in EnumerateDirectories(mPath) do
  begin
    var full_sub_path := DirectoryInfo.Create(sub_path).FullName;
    var size := ScanCurrentFolder(full_sub_path);
    all_scanned_folders += new mFolderData(full_sub_path, size);
    Result += size;
  end;
  
  foreach var fname in EnumerateFiles(mPath) do
  begin
    var size := FileInfo.Create(fname).Length;
    all_scanned_files += new mFileData(size);
    Result += size;
  end;
  
end;

begin
  $'Size: {ScanCurrentFolder(''testfolder'')}'.Println;
  $'files: {all_scanned_files.Count}'.Println;
  $'folders: {all_scanned_folders.Count}'.Println;
end.

Благодарю за разъяснение.

Получается, что без промежуточной переменной не обойтись и это совсем не костыль, а вполне себе рабочее решение.

Однако мне всё же непонятно, чем отличаются переменные mTmp и mFoldersList[mCountAllFolders - 1].sizefolder. В обоих случаях ссылка на них берется ДО вызова ScanCurrentFolder, но почему-то для локальной переменной ссылка сохранилась, а для элемента глобального массива записи ссылка потерялась. Видимо существует какая-то неизвестная мне причина, по которой компилятор различает такие переменные и по-разному выделяет под них память.

Возможно эту неочевидную особенность следует отразить в справке.

Разница в том, что память на которую ссылается mFoldersList вы перевыделяете, а mTmp это локальная память - она не перевыделяется принципиально.

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

1 лайк
var a := new byte[1](1);

function f1: byte;
begin
  a := new byte[1](2);
  Result := 3;
end;

begin
  a[0] := f1;
  Print(a[0]);
end.

Тут то же самое происходит. Обращение к a[0] идёт до вызова функции - поэтому оно обращается к старому массиву. А после этой строчки f1 уже подменило массив новым.

Вообще, это неопределённое поведение, потому что нигде не гарантируется что адрес a[0] загрузит на стек именно до вызова функции, а не после.

Таких ситуаций надо принципиально избегать. К примеру при использовании List<> - объект списка подменяться не будет. Может подменяться массив, который оно хранит внутри - но вы это никак не заметите, потому что всё инкапсулировано.

В принципе понятно.