Wird dieses dynamische Delphi-Array-Verhalten erwartet?

8

Die Frage ist: Wie werden dynamische Arrays intern von Delphi verwaltet, wenn sie als Klassenmitglied festgelegt werden? Werden sie kopiert oder als Referenz übergeben? Delphi 10.3.3 verwendet.

Die UpdateArrayMethode löscht das erste Element aus dem Array. Die Array-Länge bleibt jedoch 2. Die UpdateArrayWithParamMethode löscht auch das erste Element aus dem Array. Die Array-Länge wird jedoch korrekt auf 1 reduziert.

Hier ist ein Codebeispiel:

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;
dwrbudr
quelle
1
Dynamische Arrays werden als Referenz übergeben. Sie werden vom Compiler referenzgezählt und verwaltet. Die Dokumentation ist ziemlich gut. (Und die Details .)
Andreas Rejbrand
Warum wird die Array-Länge nach dem Aufruf von UpdateArray nicht aktualisiert?
Dwrbudr
Nein, ist es nicht. Nach dem Aufruf von recUpdate.UpdateArray beträgt die Länge (lArr)
dwrbudr
Es liegt an der DeleteProzedur. Es muss das dynamische Array neu zuweisen, und daher müssen sich alle Zeiger darauf "bewegen". Aber es kennt nur einen dieser Hinweise, nämlich den, den Sie ihm geben.
Andreas Rejbrand
Ich habe das Problem analysiert und muss zugeben, dass ich es nicht erklären kann. Es fühlt sich an wie ein Käfer. Aber vielleicht weiß David oder Remy oder jemand anderes mehr darüber als ich.
Andreas Rejbrand

Antworten:

8

Das ist eine interessante Frage!

Da Deleteändert sich die Länge des dynamischen Arrays - genausoSetLength tut - es hat die dynamische Array neu zu verteilen. Außerdem wird der ihm zugewiesene Zeiger auf diesen neuen Speicherort geändert. Aber offensichtlich kann es keine anderen Zeiger auf das alte dynamische Array ändern.

Daher sollte die Referenzanzahl des alten dynamischen Arrays verringert und ein neues dynamisches Array mit einer Referenzanzahl von 1 erstellt werden. Der angegebene Zeiger Deletewird auf dieses neue dynamische Array gesetzt.

Daher sollte das alte dynamische Array unberührt bleiben (mit Ausnahme der reduzierten Referenzanzahl natürlich). Dies ist im Wesentlichen für die ähnliche SetLengthFunktion dokumentiert :

Im Anschluss an einen Anruf SetLength, Sgarantiert eine eindeutige Zeichenfolge oder ein Array verweisen - , die einen String oder ein Array mit einem Referenzzähler ist.

Aber überraschenderweise passiert dies in diesem Fall nicht ganz.

Betrachten Sie dieses minimale Beispiel:

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;

Ich habe die Werte so gewählt, dass sie leicht im Speicher zu erkennen sind (Alt + Strg + E).

Nach (1) averweist $02A2C198in meinem Testlauf auf:

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

Hier beträgt der Referenzzähler 2 und die Arraylänge erwartungsgemäß 2. (Informationen zum internen Datenformat für dynamische Arrays finden Sie in der Dokumentation .)

Nach (2) a = b, das heißt Pointer(a) = Pointer(b). Beide zeigen auf dasselbe dynamische Array, das jetzt so aussieht:

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

Wie erwartet beträgt der Referenzzähler jetzt 3.

Nun wollen wir sehen, was nach (3) passiert. azeigt jetzt auf ein neues dynamisches Array 2A30F88in meinem Testlauf:

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

Wie erwartet hat dieses neue dynamische Array einen Referenzzähler von 1 und nur das "B-Element".

Ich würde erwarten, dass das alte dynamische Array, auf das bimmer noch zeigt, wie zuvor aussieht, jedoch mit einer reduzierten Referenzanzahl von 2. Aber jetzt sieht es so aus:

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

Obwohl der Referenzzähler tatsächlich auf 2 reduziert ist, wurde das erste Element geändert.

Mein Fazit ist das

(1) Es ist Bestandteil des DeleteVerfahrensvertrags, dass alle anderen Verweise auf das ursprüngliche dynamische Array ungültig werden.

oder

(2) Es sollte sich wie oben beschrieben verhalten. In diesem Fall handelt es sich um einen Fehler.

Leider wird dies in der Dokumentation des DeleteVerfahrens überhaupt nicht erwähnt.

Es fühlt sich an wie ein Käfer.

Update: Der RTL-Code

Ich habe mir den Quellcode der DeleteProzedur angesehen, und das ist ziemlich interessant.

Es kann hilfreich sein, das Verhalten mit dem von zu vergleichen SetLength(da dieses korrekt funktioniert):

  1. Wenn der Referenzzähler des dynamischen Arrays 1 ist, wird SetLengtheinfach versucht, die Größe des Heap-Objekts zu ändern (und das Längenfeld des dynamischen Arrays zu aktualisieren).

  2. Andernfalls SetLengthwird eine neue Heap-Zuordnung für ein neues dynamisches Array mit einem Referenzzähler von 1 vorgenommen. Der Referenzzähler des alten Arrays wird um 1 verringert.

Auf diese Weise wird garantiert, dass der endgültige Referenzzähler immer ist 1 - entweder von Anfang an oder es wurde ein neues Array erstellt. (Es ist gut, dass Sie nicht immer eine neue Heap-Zuordnung vornehmen. Wenn Sie beispielsweise ein großes Array mit einer Referenzanzahl von 1 haben, ist es billiger, es einfach abzuschneiden, als es an einen neuen Speicherort zu kopieren.)

Da Deletedas Array immer kleiner wird, ist es verlockend, einfach zu versuchen, die Größe des Heap-Objekts dort zu reduzieren, wo es sich befindet. Und genau das versucht der RTL-Code System._DynArrayDelete. Daher wird in Ihrem Fall das BBBBBBBBan den Anfang des Arrays verschoben. Alles ist gut.

Aber dann ruft es an System.DynArraySetLength, was auch von verwendet wird SetLength. Und dieses Verfahren enthält den folgenden Kommentar:

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

bevor festgestellt wird, dass das Objekt tatsächlich gemeinsam genutzt wird (in unserem Fall ref count = 3), wird eine neue Heap-Zuordnung für ein neues dynamisches Array vorgenommen und das alte (reduzierte) an diesen neuen Speicherort kopiert. Es reduziert die Ref-Anzahl des alten Arrays und aktualisiert die Ref-Anzahl, Länge und den Argumentzeiger des neuen Arrays.

Also haben wir trotzdem ein neues dynamisches Array erhalten. Die RTL-Programmierer haben jedoch vergessen, dass sie das ursprüngliche Array, das jetzt aus dem neuen Array besteht, das über dem alten Array liegt, bereits durcheinander gebracht haben : BBBBBBBB BBBBBBBB.

Andreas Rejbrand
quelle
Danke Andreas! Um auf der sicheren Seite zu sein, verwende ich nur Zeiger auf dynamische Arrays. Ich habe getestet, wie dieselben Fälle mit Zeichenfolgen behandelt werden, und es funktioniert wie (2), z. B. wird eine neue Zeichenfolge zugewiesen (Kopie beim Schreiben) und die ursprüngliche (die lokale Variable) bleibt unberührt.
Dwrbudr
1
@dwrbudr: Persönlich würde ich die Verwendung Deletein einem dynamischen Array vermeiden . Zum einen ist es auf großen Arrays nicht billig (da es notwendigerweise viele Daten kopieren muss). Und diese aktuelle Ausgabe macht mir natürlich noch mehr Sorgen. Aber lassen Sie uns auch abwarten, ob die anderen Delphi-Mitglieder der SO-Community meiner Analyse zustimmen.
Andreas Rejbrand
1
@dwrbudr: Ja. Natürlich nicht dynamische Arrays Copy-on-Write - Semantik verwenden, aber Prozeduren wie SetLength, Insertund Deleteoffensichtlich neu zu verteilen müssen. Das Ändern eines Elements (wie b[2] := 4) wirkt sich auf alle anderen dynamischen Array-Variablen aus, die auf dasselbe dynamische Array verweisen. Es wird keine Kopie geben.
Andreas Rejbrand
1
Verwenden Sie keinen Zeiger auf einen Dynarray
David Heffernan
3
@ LURD: quality.embarcadero.com/browse/RSP-27870
Andreas Rejbrand