Ожидается ли это Delphi динамическое c поведение массива - PullRequest
8 голосов
/ 12 марта 2020

Вопрос в том, как динамически c массивы управляются внутренне Delphi, когда они заданы как члены класса? Они скопированы или переданы по ссылке? Delphi 10.3.3 используется.

Метод UpdateArray удаляет первый элемент из массива. Но длина массива остается равной 2. Метод UpdateArrayWithParam также удаляет первый элемент из массива. Но длина массива правильно уменьшена до 1.

Вот пример кода:

interface

type
  TSomeRec = record
      Name: string;
  end;
  TSomeRecArray = array of TSomeRec;

  TSomeRecUpdate = class
    Arr: TSomeRecArray;
    procedure UpdateArray;
    procedure UpdateArrayWithParam(var ParamArray: TSomeRecArray);
  end;

implementation

procedure TSomeRecUpdate.UpdateArray;
begin
    Delete(Arr, 0, 1);
end;

procedure TSomeRecUpdate.UpdateArrayWithParam(var ParamArray: TSomeRecArray);
begin
    Delete(ParamArray, 0, 1);
end;

procedure Test;
var r: TSomeRec;
    lArr: TSomeRecArray;
    recUpdate: TSomeRecUpdate;
begin
    lArr := [];

    r.Name := 'abc';
    lArr := lArr + [r];
    r.Name := 'def';
    lArr := lArr + [r];

    recUpdate := TSomeRecUpdate.Create;
    recUpdate.Arr := lArr;
    recUpdate.UpdateArray;
    //(('def'), ('def')) <=== this is the result of copy watch value, WHY two values?

    lArr := [];

    r.Name := 'abc';
    lArr := lArr + [r];
    r.Name := 'def';
    lArr := lArr + [r];

    recUpdate.UpdateArrayWithParam(lArr);

    //(('def')) <=== this is the result of copy watch value - WORKS

    recUpdate.Free;
end;

1 Ответ

8 голосов
/ 12 марта 2020

Это интересный вопрос!

Поскольку Delete меняет длину массива dynamici c - так же, как SetLength - он должен перераспределить массив Dynami c. И это также изменяет указатель, данный ему на это новое место в памяти. Но очевидно, что он не может изменить любые другие указатели на старый массив Dynami c.

Поэтому он должен уменьшить счетчик ссылок старого массива Dynami c и создать новый массив Dynami c со ссылкой число 1. Указатель, заданный для Delete, будет установлен на этот новый массив Dynami c.

Следовательно, старый массив Dynami c должен быть нетронутым (за исключением его уменьшенного числа ссылок, курс). Это задокументировано для аналогичной SetLength функции :

После вызова SetLength, S гарантированно ссылается на уникальную строку или массив - что is, строка или массив с количеством ссылок, равным единице.

Но, что удивительно, в данном случае это не совсем так.

Рассмотрим этот минимальный пример:

procedure TForm1.FormCreate(Sender: TObject);
var
  a, b: array of Integer;
begin

  a := [$AAAAAAAA, $BBBBBBBB]; {1}
  b := a;                      {2}

  Delete(a, 0, 1);             {3}

end;

Я выбрал значения, чтобы их было легко найти в памяти (Alt + Ctrl + E).

После (1) a указывает на $02A2C198 в моем тестовом прогоне:

02A2C190  02 00 00 00 02 00 00 00
02A2C198  AA AA AA AA BB BB BB BB

Здесь счетчик ссылок равен 2, а длина массива равна 2, как и ожидалось. (См. Документацию для внутреннего формата данных для динамических c массивов.)

После (2), a = b, то есть Pointer(a) = Pointer(b). Они оба указывают на один и тот же динамический массив c, который теперь выглядит следующим образом:

02A2C190  03 00 00 00 02 00 00 00
02A2C198  AA AA AA AA BB BB BB BB

Как и ожидалось, счетчик ссылок теперь равен 3.

Теперь посмотрим, что произойдет после (3). a теперь указывает на новый динамический c массив в 2A30F88 в моем тестовом прогоне:

02A30F80  01 00 00 00 01 00 00 00
02A30F88  BB BB BB BB 01 00 00 00

Как и ожидалось, этот новый динамический c массив имеет счетчик ссылок 1 и только «Элемент B».

Я бы ожидал, что старый массив Dynami c, на который все еще указывает b, будет выглядеть так же, как и раньше, но с уменьшенным количеством ссылок 2. Но теперь это выглядит так :

02A2C190  02 00 00 00 02 00 00 00
02A2C198  BB BB BB BB BB BB BB BB

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

Мой вывод таков:

(1) Он является частью контракт процедуры Delete о том, что он делает недействительными все другие ссылки на исходный массив Dynami c.

или

(2) Он должен вести себя так, как я обрисовал выше, в этом случае это ошибка.

К сожалению, документация для Delete процедуры вообще не упоминает об этом.

Это похоже на ошибку.

Обновление: код RTL

Я посмотрел исходный код процедуры Delete, и это довольно интересно.

Может быть полезно сравнить поведение с SetLength (потому что это работает правильно):

  1. Если счетчик ссылок Dynami c массив равен 1, SetLength пытается просто изменить размер объекта кучи (и обновить поле длины массива Dynami c).

  2. В противном случае SetLength делает новое выделение кучи для нового динамического c массива со счетчиком ссылок 1. Счетчик ссылок старого массива уменьшается на 1.

Таким образом, гарантируется, что окончательный счетчик ссылок всегда равен 1 - либо с самого начала, либо был создан новый массив. (Хорошо, что вы не всегда делаете новое выделение кучи. Например, если у вас большой массив со счетчиком ссылок, равным 1, просто укоротить его дешевле, чем скопировать его в новое место.)

Теперь, поскольку Delete всегда уменьшает размер массива, заманчиво попытаться просто уменьшить размер кучи, где он находится. И это действительно то, что пытается сделать код RTL в System._DynArrayDelete. Следовательно, в вашем случае BBBBBBBB перемещается в начало массива. Все хорошо.

Но тогда он вызывает System.DynArraySetLength, что также используется SetLength. И эта процедура содержит следующий комментарий,

// If the heap object isn't shared (ref count = 1), just resize it. Otherwise, we make a copy

прежде чем он обнаружит, что объект действительно является общим (в нашем случае ref count = 3), сделает новое выделение кучи для нового динамического c массива и скопирует старый (уменьшенный) в это новое местоположение. Он уменьшает счетчик ссылок старого массива и обновляет счетчик ссылок, длину и указатель аргумента нового.

Таким образом, мы все равно получили новый динамический массив c. Но программисты RTL забыли, что они уже испортили исходный массив, который теперь состоит из нового массива, помещенного поверх старого: BBBBBBBB BBBBBBBB.

...