Использую рекурсию.
Получилось так, что при рекурсивном вызове функции поместить полученное значение в массив (запись) получается только через промежуточную переменную.
Как такое возможно и в чем может быть причина?
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.
Максимально сократил код.
На форме только кнопка и диалог выбора папки.
Для проверки один из вариантов (рабочий/нерабочий) необходимо закомментировать.
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.
Всё считает как ожидалось.
Надо таки на вашей стороне абсолютно минимальный код найти, чтобы понять что у вас не так как у меня работает.
Только, для начала - у вас версия паскаля последняя стоит? Скачать
Так а зачем вам изначальный смысл сохранять? Надо найти минимальный код который даёт неожидаемое поведение, а не тот - который считает то же самое, что вы пытались посчитать в оригинальной программе.
В первую очередь надо вообще от WinForms избавится. Сделать, чтобы в отдельной консольной программе, состоящей из 1 файла, такое же поведение получалось.
Тогда поиск минимального кода на много быстрее пойдёт. Не надо будет при каждом запуске выбирать папку, для которой считать - запускаете и сразу смотрите, ожидаемое-ли кол-во байт вывело.
И саму папку для которой считаете - стоит минимизировать. Я так понимаю, нужно только чтобы в ней была 1 подпапка с 1 файлом - тогда проблемная строчка будет выполняться только 1 раз.
На самом деле вы могли и на форум залить… Тянете файл-архив и кидаете прямо в окно ответа в браузере - он сразу прикрепится.
Мда, покопался в сгенерированных .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, но почему-то для локальной переменной ссылка сохранилась, а для элемента глобального массива записи ссылка потерялась. Видимо существует какая-то неизвестная мне причина, по которой компилятор различает такие переменные и по-разному выделяет под них память.
Возможно эту неочевидную особенность следует отразить в справке.
В данной ситуации другое в приоритет выходит - тут бы перепродумать стиль кода, потому что текущий код требует таких костылей.
Вы, по сути, сами из под себя ковёр выдёргиваете в процессе выполнения. С таким кол-вом побочных эффектов у функций - вам часто будут нужны какие то костыли для обхода крайних ситуаций, которые вы сами создали.
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<> - объект списка подменяться не будет. Может подменяться массив, который оно хранит внутри - но вы это никак не заметите, потому что всё инкапсулировано.