Массив или список Delphi не обновляются должным образом в цикле for

avatar
user14116089
1 июля 2021 в 19:47
233
1
-1

У меня есть цикл, который обновляет значения в одномерном массиве (Arr2), а затем добавляет массив в список (ResultPnts):

type
  point = packed record
    case aSInt of
       0: (x, y, z: aFloat);
       1: (v: array [0 .. 2] of aFloat); { vertex }
  end;

  MyPntArr = array of point;

  EntPntArr = record
    enttyp : byte;
    zb, zh : double;    // store zbase and zheight for 2d lines/arcs
    closed : boolean;   // set true if first and last points of lines/arcs are equal
    Pnts   : MyPntArr;
  end;

  tPaths = TList<EntPntArr>;

procedure PathEntToPntArr (ent : entity; var resultPnts : tPaths);
var
  Arr1, Arr2 : EntPntArr;
  j : integer;
begin
  ...                
  setlength (Arr2.Pnts, 2);
  Arr2.closed := false;
  Arr2.enttyp := entslb;
  for j := 0 to length(Arr1.Pnts)-1-ord(Arr1.closed) do begin
    setlength (Arr2.Pnts, 2); // note: I'm not entirely sure why, but without setting length in
                              // each iteration of the loop, the array points are not updated
                              // in each subsequent iteration after the first.
    Arr2.Pnts[0] := Arr1.Pnts[j];
    // add slbthick to Arr1.Pnts[j], with result in the var parameter Arr2.Pnts[1]
    AddPnt (Arr1.Pnts[j], slbthick, Arr2.Pnts[1]);  
    resultPnts.Add(Arr2); 
  end;

Изначально у меня не было вызова setlength в начале цикла for, и в этом случае соответствующее количество элементов добавлялось к resultPnts, но все они были одинаковыми (как если бы логика присваивания Arr2.Pnts [0] и Arr2.Pnts[1] не вызывались в каждой итерации цикла)

Я исправил проблему, добавив вызов setlength, но я действительно не понимаю, зачем это нужно. Я хотел бы понять, что здесь происходит, чтобы избежать подобных проблем в будущем.

Кто-нибудь может объяснить мне, почему код не работает должным образом без setlength в цикле?

Источник
SilverWarior
1 июля 2021 в 20:54
0

Я считаю, что проблема может заключаться в Arr2.Pnts[0] := Arr1.Pnts[j];, с помощью которого вы всегда меняете значение первой точки вашего Arr2.Pnts. Но позже в вашем AddPnt (Arr1.Pnts[j], slbthick, Arr2.Pnts[1]); вы проверяете значение второй точки вашего Arr2.Pnts.

Ответы (1)

avatar
Remy Lebeau
1 июля 2021 в 20:57
2

Динамические массивы имеют подсчет ссылок.

Без SetLength() внутри цикла вы добавляете несколько копий переменной Arr2 в resultPnts, но все они ссылаются на один и тот же физический массив в памяти, который вы изменяете в памяти. каждой итерации цикла. Вот почему все записи заканчиваются одинаковыми значениями массива, присвоенными последней итерацией цикла.

  • Перед входом в цикл Arr2.Pnts указывает на массив с refcount=1 из начального SetLength().
  • Первая итерация цикла изменяет содержимое существующего массива, затем добавляет копию Arr2 в resultPnts, увеличивая refcount массива до 2.
  • Вторая итерация цикла изменяет содержимое существующего массива, затем добавляет копию Arr2 в resultPnts, увеличивая refcount массива до 3.
  • И так далее, пока цикл не закончится.
  • При выходе PathEntToPntArr() очищается только ссылка в Arr2.Pnts, уменьшая значение refcount массива, но массив по-прежнему имеет активные ссылки из всех записей в resultPnts. Массив будет освобожден, когда все эти ссылки будут удалены позже.

SetLength() имеет побочный эффект: он переводит существующий динамический массив в refcount=1, если новый размер > 0, даже если существующий размер массива имеет такое же значение. Если массив имеет refcount=1, SetLength() изменит массив на месте, в противном случае он уменьшит значение refcount массива, а затем выделит новый массив с refcount=1.

Таким образом, с SetLength() внутри цикла, каждая из ваших записей resultPnts будет иметь уникальный массив, назначенный им.

  • Перед входом в цикл Arr2.Pnts указывает на массив с refcount=1 из начального SetLength().
  • Первая итерация цикла вызывает SetLength(), оставляя текущий массив нетронутым, затем изменяет содержимое этого массива, затем добавляет копию Arr2 в resultPnts, увеличивая refcount этого массива до 2.<8264200156 >
  • Вторая итерация цикла вызывает SetLength(), уменьшает текущий массив refcount до 1 и создает новый массив с refcount=1, затем изменяет содержимое этого нового массива, затем добавляет копию Arr2 в resultPnts, увеличивая этот массив refcount до 2.
  • И так далее, пока цикл не закончится.
  • При выходе PathEntToPntArr() очищается только ссылка в Arr2.Pnts, уменьшая refcount последнего созданного массива. Каждый массив в resultPnts будет освобожден индивидуально, так как его записи будут очищены позже.

В RTL в XE7+ есть общедоступная функция DynArrayUnique(), которая служит той же цели, что и ваш вызов SetLength(), например:

procedure PathEntToPntArr (ent : entity; var resultPnts : tPaths);
var
  Arr1, Arr2 : EntPntArr;
  j : integer;
begin
  ...                
  SetLength(Arr2.Pnts, 2);
  ...
  for j := 0 to length(Arr1.Pnts)-1-ord(Arr1.closed) do begin
    DynArrayUnique(Pointer(Arr2.Pnts), TypeInfo(point)); // <--
    ...
    resultPnts.Add(Arr2); 
  end;
  ...
end;

Или вместо этого можно использовать System.Copy():

procedure PathEntToPntArr (ent : entity; var resultPnts : tPaths);
var
  Arr1, Arr2 : EntPntArr;
  j : integer;
begin
  ...                
  SetLength(Arr2.Pnts, 2);
  ...
  for j := 0 to length(Arr1.Pnts)-1-ord(Arr1.closed) do begin
    Arr2.Pnts := Copy(Arr2.Pnts{0, Length(Arr2.Pnts)}); // <--
    ...
    resultPnts.Add(Arr2); 
  end;
  ...
end;
user14116089
1 июля 2021 в 22:00
0

Спасибо Реми за объяснение. Я ожидал чего-то в этом роде, но не мог себе этого объяснить. Я предполагаю, что одна из моих проблем заключалась в том, было ли добавление setlength надежным решением, поэтому вы решили все проблемы, которые у меня были на этот счет. Не уверен, есть ли какие-либо различия в эффективности между setlength, dynarrayunique или copy, но ожидайте, что это не будет большой разницей и не будет иметь значения в контексте моего проекта.

user14116089
1 июля 2021 в 22:42
0

(изменил мой код, чтобы использовать DynArrayUnique, так как причина его появления немного более ясна. Работает хорошо - еще раз спасибо!)

David Heffernan
2 июля 2021 в 08:18
0

Я не очень уверен, что мне нравится отсутствие безопасности типов в DynArrayUnique. Лично я бы предпочел Copy или TArray<point>.Create(...) (при условии, что вы переключаетесь на общие массивы, что в любом случае является хорошей практикой. Не уверен, что MyPntArr.Create(...) компилируется или нет.

Remy Lebeau
2 июля 2021 в 14:08
0

@DavidHeffernan "Не уверен, что MyPntArr.Create(...) компилируется или нет " - да, должно, согласно документации.

user14116089
2 июля 2021 в 19:54
0

Спасибо за постоянные комментарии ... Я буду следить за комментариями относительно универсальных массивов (с которыми я должен признать, что я не очень хорошо знаком, но они, похоже, предлагают некоторое преимущество)