このDelphi動的配列の動作は期待されていますか


8

問題は、動的配列がクラスメンバーとして設定されている場合、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
動的配列は参照渡しされます。これらは参照カウントされ、コンパイラーによって管理されます。ドキュメントはかなり良いです。(そして詳細。)
Andreas Rejbrand

次に、UpdateArrayが呼び出された後、なぜ配列の長さが更新されないのですか?
dwrbudr

いいえ、そうではありません。recUpdate.UpdateArrayが呼び出された後、Length(lArr)は2
dwrbudr

それはDelete手順のせいです。動的配列を再割り当てする必要があるため、その配列へのすべてのポインタを移動する必要があります。しかし、それはこれらのポインタの1つ、つまりあなたがそれに与えるものだけを知っています。
Andreas Rejbrand

私は問題を分析しました、そしてそれを説明することができないことを認めなければなりません。それはバグのように感じます。しかし、DavidやRemy、あるいは他の誰かが私よりもこのことについて知っているかもしれません。
Andreas Rejbrand

回答:


8

これは興味深い質問です。

動的配列のDelete長さを変更するので- 同様に-動的配列を再割り当てする必要があります。また、指定されたポインタをメモリ内のこの新しい場所に変更します。しかし、明らかにそれは古い動的配列への他のポインタを変更することはできません。SetLength

したがって、古い動的配列の参照カウントを減らし、参照カウントが1の新しい動的配列を作成する必要があります。指定されたポインターはDelete、この新しい動的配列に設定されます。

したがって、古い動的配列は変更しないでください(もちろん、参照カウントが減少することを除きます)。これは基本的に同様のSetLength機能について文書化されています

呼び出しに続いてSetLengthS一つの参照カウントを持つ文字列または配列である-一意の文字列や配列を参照することが保証されます。

しかし、驚くべきことに、この場合、これはまったく起こりません。

この最小限の例を考えてみましょう:

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です。(動的配列の内部データ形式については、ドキュメントを参照してください。)

(2)後、a = b、すなわち、Pointer(a) = Pointer(b)。どちらも同じ動的配列を指しており、次のようになります。

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

予想どおり、参照カウントは3になりました。

では、(3)の後に何が起こるか見てみましょう。aここで2A30F88、私のテスト実行で新しい動的配列をポイントします:

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

予想どおり、この新しい動的配列の参照カウントは1で、「B要素」のみです。

b以前のようにまだ指している古い動的配列が以前と同じように見えるが、参照カウントが2に減ると予想しますが、現在は次のようになっています。

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

参照カウントは確かに2に減っていますが、最初の要素が変更されています。

私の結論は

(1)Delete初期動的配列への他のすべての参照を無効にすることは、手順のコントラクトの一部です。

または

(2)上で概説したように動作するはずですが、その場合はバグです。

残念ながら、Delete手順のドキュメントではこれについてまったく触れられていません。

まるでバグのようです。

更新:RTLコード

Delete手順のソースコードを見てみましたが、これはかなりおもしろいです。

SetLength(正常に動作するため)動作と動作を比較すると役立つ場合があります。

  1. 動的配列の参照カウントが1の場合SetLength、ヒープオブジェクトのサイズを変更しようとします(動的配列の長さフィールドを更新します)。

  2. それ以外の場合はSetLength、参照カウントが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)、新しい動的配列に新しいヒープ割り当てを作成し、古い(縮小された)配列をこの新しい場所にコピーします。古い配列の参照カウントを減らし、新しい配列の参照カウント、長さ、引数ポインタを更新します。

したがって、とにかく新しい動的配列ができました。しかし、RTLプログラマーは、元の配列をすでにめちゃくちゃにしていたことを忘れており、これは古い配列の上に配置された新しい配列で構成されていますBBBBBBBB BBBBBBBB


アンドレアス、ありがとう!安全のために、動的配列へのポインタを使用します。同じケースが文字列でどのように処理され、それが(2)として機能するかをテストしました。たとえば、新しい文字列が割り当てられ(書き込み時にコピー)、元の文字列(ローカル変数)は変更されません。
dwrbudr

1
@dwrbudr:個人的にはDelete、動的配列での使用は避けます。1つには、大規模な配列では安価ではありません(大量のデータをコピーする必要があるため)。そして、この現在の問題は、明らかに私をさらに心配させます。しかし、SOコミュニティの他のDelphiメンバーが私の分析に同意するかどうかも見てみましょう。
Andreas Rejbrand

1
@dwrbudr:はい。もちろん、動的配列は、コピー・オン・ライトセマンティクスを使用していないが、同様な手順SetLengthInsertDelete明らかに再割り当てする必要があります。要素の変更(などb[2] := 4)は、同じ動的配列を指す他の動的配列変数に影響します。コピーはありません。
Andreas Rejbrand

1
dynarrayへのポインターを使用しないでください
David Heffernan

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.