Общий список записей, содержащий динамический массив

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

Type
  TMyRec=record
MyArr:Array of Integer;
    Name: string;
    Completed: Boolean;
  end;

var
  MyList:TList<TMyRec>;
  MyRec:TMyRec;

затем я создаю список и устанавливаю длину массива следующим образом

MyList:=TList<TMyRec>.Create;
SetLength(MyRec.MyArr,5);
MyRec.MyArr[0]:=8;  // just for demonstration
MyRec.Name:='Record 1';
MyRec.Completed:=true;
MyList.Add(MyRec);

затем я меняю данные в MyArr, а также меняю MyRec.Name и добавляю еще один элемент в список

MyRec.MyArr[0]:=5;  // just for demonstration
MyRec.Name:='Record 2';
MyRec.Completed:=false;
MyList.Add(MyRec);

когда MyRec.MyArr изменяется после добавления первого элемента в список, MyArr, который хранится в списке, также изменяется. однако другие поля записи - нет.

Мой вопрос заключается в том, как предотвратить отражение изменений в MyRec.MyArr в массиве, который уже хранится в элементе списка.

мне нужно объявить несколько записей.


person Khalid    schedule 31.01.2014    source источник


Ответы (3)


Этот пример можно упростить, удалив все ссылки на дженерики:

{$APPTYPE CONSOLE}

var
  x, y: array of Integer;

begin
  SetLength(x, 1);
  x[0] := 42;
  y := x;
  Writeln(x[0]);
  y[0] := 666;
  Writeln(x[0]);
end.

Результат:

42
666

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

Вы можете решить эту проблему, заставив ссылку быть уникальной (то есть иметь только простую ссылку). Есть несколько способов добиться этого. Например, вы можете вызвать SetLength для массива, который вы хотите сделать уникальным.

{$APPTYPE CONSOLE}

var
  x, y: array of Integer;

begin
  SetLength(x, 1);
  x[0] := 42;
  y := x;
  SetLength(y, Length(y));
  Writeln(x[0]);
  y[0] := 666;
  Writeln(x[0]);
end.

Выход:

42
42

Итак, в вашем коде вы можете написать это так:

MyList:=TList<TMyRec>.Create;

SetLength(MyRec.MyArr,5);
MyRec.MyArr[0]:=8;  // just for demonstration
MyRec.Name:='Record 1';
MyRec.Completed:=true;
MyList.Add(MyRec);

SetLength(MyRec.MyArr,5); // <-- make the array unique
MyRec.MyArr[0]:=5;  // just for demonstration
MyRec.Name:='Record 2';
MyRec.Completed:=false;
MyList.Add(MyRec);

Вы можете использовать множество других способов обеспечения уникальности, включая Finalize, назначение nil, Copy и т. д.

Эта проблема подробно описана в документации. Вот соответствующие выдержки:

Если X и Y являются переменными одного и того же типа динамического массива, X := Y указывает X на тот же массив, что и Y. (Перед выполнением этой операции нет необходимости выделять память для X.) В отличие от строк и статических массивов, копирование -on-write не используется для динамических массивов, поэтому они не копируются автоматически перед записью. Например, после выполнения этого кода:

 var
   A, B: array of Integer;
   begin
     SetLength(A, 1);
     A[0] := 1;
     B := A;
     B[0] := 2;
   end;

значение A[0] равно 2. (Если бы A и B были статическими массивами, A[0] все равно было бы равно 1.) Присвоение индекса динамического массива (например, MyFlexibleArray[2] := 7) не перераспределить массив. Индексы вне допустимого диапазона не сообщаются во время компиляции. Напротив, чтобы сделать независимую копию динамического массива, вы должны использовать глобальную функцию копирования:

 var
   A, B: array of Integer;
 begin
   SetLength(A, 1);
   A[0] := 1;
   B := Copy(A);
   B[0] := 2; { B[0] <> A[0] }
 end;
person David Heffernan    schedule 31.01.2014
comment
+1, мне любопытно, почему нет прямого языкового элемента, чтобы сделать глубокую копию. Clone() было бы хорошим именем. Или ввести динамические массивы COW (которые у меня уже есть). - person LU RD; 31.01.2014
comment
@LURD Глубокое копирование — это одна из тех вещей, которые легко сказать, но очень сложно сделать. На уровне языка вам понадобится каждый отдельный тип, чтобы иметь возможность выполнять глубокое копирование. Что бы это значило для указателя? Возможно, типизированный указатель выполним, но как насчет нетипизированного указателя? Как глубоко скопировать интерфейс? Так что я думаю, что это внутренне тяжело. Спасибо за уф кстати. Голосование здесь странное, я должен сказать. Больше голосов за отсутствие ответа. - person David Heffernan; 31.01.2014
comment
@lurd может быть потому, что строки состоят из простых символов, а массивы могут состоять из объектов, интерфейсов и черт знает чего. Это было бы сокрытием сложности, ведущей даже к несбывшимся ожиданиям, подобным тому, с которого начинается эта тема. - person Arioch 'The; 31.01.2014
comment
Для строк, интерфейсов и тому подобного просто увеличьте количество ссылок. Нетипизированные указатели останутся в дикой природе, но не будут отличаться от команды Copy(). - person LU RD; 31.01.2014
comment
@LURD Это не будет глубокой копией. Глубокое копирование подразумевает копирование значений. - person David Heffernan; 31.01.2014
comment
@DavidHeffernan, потому что это сложно, именно поэтому такая отговорка, что VCL имеет функцию копирования, но не функцию глубокого копирования. Вы программируете на Python, поэтому знаете, что Python не отказывается от функции глубокого копирования. Тот факт, что в Delphi есть некоторые крайние случаи, не означает, что функция глубокого копирования не должна существовать. Глубокая копия Python также не копирует некоторые метаданные. И да, каждый отдельный тип должен иметь возможность выполнять глубокое копирование и сериализацию, пока они это делают. - person alcalde; 01.02.2014
comment
@Arioch'Другие языки не уклоняются от глубокого копирования. На самом деле необычное поведение было бы связано не с функцией глубокого копирования, а с собственным примером Дэвида, где массив неявно выполняет копирование при записи, когда выполняется функция SetLength. На мой взгляд, это нарушение принципа наименьшего удивления с точки зрения дизайна языка. - person alcalde; 01.02.2014
comment
@alcalde Типы Python должны реализовывать хуки для работы deepcopy. Крючки хорошо определены, но они требуют сотрудничества от актеров. - person David Heffernan; 01.02.2014

...здесь были наблюдения за разногласиями по исходному вопросу

Что касается остальных, я бы предпочел разорвать связь между вашей переменной и списком сразу после того, как вы добавили значение. Через несколько месяцев вы бы забыли об этой проблеме и, возможно, провели бы рефакторинг своей программы. Если вы поместите второй SetLength отдельно от List.Add, вы можете просто забыть, что запись по-прежнему содержит ссылку на тот же массив, который у вас есть в списке.

  TMyRec=record
    MyArr: TArray< double >; // making it 1D for simplicity
    Name: string;
    Completed: Boolean;
  end;


SetLength(MyRec.MyArr,5);
MyRec.MyArr[0]:=8;  // just for demonstration
MyRec.Name:='Record 1';
MyRec.Completed:=true;
MyList.Add(MyRec);
MyRec.MyArr := nil; // breaking the parasite link immediately!

...теперь тут можно делать что угодно - но MyRec уже чист.

Тогда что, если бы у вас было много массивов, а не один? Есть одна функция, которую Delphi использует за кулисами: http://docwiki.embarcadero.com/Libraries/XE5/en/System.Finalize, который нашел бы все массивы для очистки.

SetLength(MyRec.MyArr,5);
MyRec.MyArr[0]:=8;  // just for demonstration
MyRec.Name:='Record 1';
MyRec.Completed:=true;
MyList.Add(MyRec);
Finalyze(MyRec); // breaking all the parasite links immediately!

Теперь последним вариантом будет просто сжать используемый код в процедуру, которую вы сможете вызывать несколько раз. Тогда переменная станет локальной, и Delphi автоматически Finalize сделает ее за вас.

Procedure AddRec( const Name: string; const Compl: boolean; const Data: array of double);
var i: integer; MyRec: TMyRec;
begin
  SetLength(MyRec.MyArr, Length( Data ) );
  for i := 0 to Length(Data) - 1 do
    MyRec.MyArr[i] := Data [i];  

  MyRec.Name := Name;

  MyRec.Completed := Compl;
  MyList.Add(MyRec);
end;

MyList:=TMyList<TMyRec>.create;

AddRec( 'Record 1', True , [ 8 ]);
AddRec( 'Record 2', False, [ 5 ]);
...

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

person Arioch 'The    schedule 31.01.2014
comment
Я просто исправил вопрос - person David Heffernan; 31.01.2014
comment
@DavidHeffernan, но вы не знаете, что на самом деле имел в виду автор темы. Две части противоречат друг другу, и вы взяли на себя смелость решить самостоятельно, ЧТО из них было неправильным. Я вижу, что оно того стоит, если он откажется от переполнения стека, поэтому было бы целесообразно сделать вопрос полезным для всех, а не для сбежавшего TS. Однако ТС здесь, и он может решить вопрос в том направлении, которое действительно является его намерением. - person Arioch 'The; 31.01.2014
comment
это моя ошибка. MyArr: array of integer - правильное замедление. Вопрос изменен Дэвидом - person Khalid; 31.01.2014
comment
@DavidHeffernan Можете ли вы добавить nil, finalyze и local var в свой ответ? тогда мой можно удалить - person Arioch 'The; 31.01.2014
comment
@DavidHeffernan ммм, упоминание о том, что они существуют, не показывает примеры и не объясняет минусы и плюсы. Не могу согласиться, что вы это уже сделали :-) и локальные вары даже не упоминаются. Ни один из них не прерывает связь немедленно. - person Arioch 'The; 31.01.2014
comment
Не стесняйтесь редактировать мой ответ, если он вам не нравится. А пока я попытаюсь перечислить все возможные решения этой проблемы, чтобы иметь возможность написать их полные демонстрации. - person David Heffernan; 31.01.2014
comment
Я также не понимаю, почему вы критикуете мой ответ за предполагаемую неполноту. Вы даже не объясняете проблему. - person David Heffernan; 31.01.2014
comment
@DavidHeffernan Я начал это как подробный комментарий к проблемам в вопросе. Затем я добавил несколько примеров, начиная с того места, где вы остановились. Это не было задумано как полный ответ, поэтому я хочу их объединить. - person Arioch 'The; 31.01.2014
comment
Иди и сделай это, если хочешь - person David Heffernan; 31.01.2014

Просто создайте новую в старой переменной, все должно быть в порядке,

MyList:=TList<TMyRec>.Create;
SetLength(MyRec.MyArr,5);
MyRec.MyArr[0]:=8;  // just for demonstration
MyRec.Name:='Record 1';
MyRec.Completed:=true;
MyList.Add(MyRec);

MyRec := TMyRec.Create();
SetLength(MyRec.MyArr,5);

MyRec.MyArr[0]:=5;  // just for demonstration
MyRec.Name:='Record 2';
MyRec.Completed:=false;
MyList.Add(MyRec);
person Reza    schedule 31.01.2014
comment
TMyRec — это запись. У него нет метода Create. И ответ без объяснения никогда не бывает идеальным. Программирование должно быть связано с пониманием, а не с магическими заклинаниями. - person David Heffernan; 31.01.2014