¿Se espera este comportamiento dinámico de matriz de Delphi?

8

La pregunta es: ¿cómo gestionan Array dinámicamente las matrices dinámicas cuando se configuran como miembros de la clase? ¿Se copian o se pasan por referencia? Delphi 10.3.3 utilizado.

El UpdateArraymétodo elimina el primer elemento de la matriz. Pero la longitud de la matriz permanece 2. El UpdateArrayWithParammétodo también elimina el primer elemento de la matriz. Pero la longitud de la matriz se reduce correctamente a 1.

Aquí hay un ejemplo de código:

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
fuente
1
Las matrices dinámicas se pasan por referencia. Son contados por referencia y administrados por el compilador. La documentación es bastante buena. (Y los detalles .)
Andreas Rejbrand
Entonces, ¿por qué la longitud de la matriz no se actualiza después de llamar a UpdateArray?
dwrbudr
No, no lo es. Después de llamar a recUpdate.UpdateArray, la longitud (lArr) es 2
dwrbudr
Es por el Deleteprocedimiento. Tiene que reasignar la matriz dinámica, por lo que todos los punteros a ella "deben" moverse. Pero solo conoce uno de estos indicadores, a saber, el que le das.
Andreas Rejbrand
He analizado el problema y debo admitir que no puedo explicarlo. Se siente como un error. Pero tal vez David o Remy u otra persona sepan más sobre esto que yo.
Andreas Rejbrand

Respuestas:

8

¡Esta es una pregunta interesante!

Dado que Deletecambia la longitud de la matriz dinámica , al igual que lo SetLengthhace, tiene que reasignar la matriz dinámica. Y también cambia el puntero que se le asigna a esta nueva ubicación en la memoria. Pero obviamente no puede cambiar ningún otro puntero a la matriz dinámica anterior.

Por lo tanto, debería disminuir el recuento de referencia de la matriz dinámica anterior y crear una nueva matriz dinámica con un recuento de referencia de 1. El puntero dado a Deletese establecerá en esta nueva matriz dinámica.

Por lo tanto, la matriz dinámica anterior debe estar intacta (a excepción de su recuento de referencia reducido, por supuesto). Esto está esencialmente documentado para la SetLengthfunción similar :

Después de una llamada a SetLength, Sse garantiza que hace referencia a una cadena o matriz única, es decir, una cadena o matriz con un recuento de referencia de uno.

Pero sorprendentemente, esto no sucede en este caso.

Considere este ejemplo mínimo:

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;

Elegí los valores para que sean fáciles de detectar en la memoria (Alt + Ctrl + E).

Después de (1), aseñala $02A2C198en mi ejecución de prueba:

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

Aquí el recuento de referencia es 2 y la longitud de la matriz es 2, como se esperaba. (Consulte la documentación del formato de datos interno para matrices dinámicas).

Después de (2), a = b, es decir, Pointer(a) = Pointer(b). Ambos apuntan a la misma matriz dinámica, que ahora se ve así:

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

Como se esperaba, el recuento de referencia ahora es 3.

Ahora, veamos qué sucede después de (3). aahora apunta a una nueva matriz dinámica 2A30F88en mi ejecución de prueba:

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

Como se esperaba, esta nueva matriz dinámica tiene un recuento de referencia de 1 y solo el "elemento B".

Esperaría que la matriz dinámica anterior, que btodavía apunta, se vea como antes pero con un recuento de referencia reducido de 2. Pero ahora se ve así:

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

Aunque el recuento de referencia se reduce a 2, el primer elemento ha cambiado.

Mi conclusión es que

(1) Es parte del contrato del Deleteprocedimiento que invalida todas las demás referencias a la matriz dinámica inicial.

o

(2) Debería comportarse como describí anteriormente, en cuyo caso esto es un error.

Desafortunadamente, la documentación del Deleteprocedimiento no menciona esto en absoluto.

Se siente como un error.

Actualización: el código RTL

Eché un vistazo al código fuente del Deleteprocedimiento, y esto es bastante interesante.

Puede ser útil comparar el comportamiento con el de SetLength(porque ese funciona correctamente):

  1. Si el recuento de referencia de la matriz dinámica es 1, SetLengthintenta simplemente cambiar el tamaño del objeto de montón (y actualizar el campo de longitud de la matriz dinámica).

  2. De lo contrario, SetLengthrealiza una nueva asignación de montón para una nueva matriz dinámica con un recuento de referencia de 1. El recuento de referencia de la matriz anterior se reduce en 1.

De esta forma, se garantiza que el recuento de referencia final sea siempre 1, ya sea desde el principio o se ha creado una nueva matriz. (Es bueno que no siempre haga una nueva asignación de almacenamiento dinámico. Por ejemplo, si tiene una matriz grande con un recuento de referencia de 1, simplemente truncarla es más barato que copiarla en una nueva ubicación).

Ahora, como Deletesiempre hace que la matriz sea más pequeña, es tentador intentar simplemente reducir el tamaño del objeto de montón donde está. Y esto es de hecho lo que intenta el código RTL System._DynArrayDelete. Por lo tanto, en su caso, BBBBBBBBse mueve al principio de la matriz. Todo está bien.

Pero luego llama System.DynArraySetLength, que también es utilizado por SetLength. Y este procedimiento contiene el siguiente comentario,

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

antes de detectar que el objeto está compartido (en nuestro caso, rec count = 3), realiza una nueva asignación de montón para una nueva matriz dinámica y copia la antigua (reducida) en esta nueva ubicación. Reduce el recuento de referencias de la matriz anterior y actualiza el recuento de referencias, la longitud y el puntero de argumento de la nueva.

Así que terminamos con una nueva matriz dinámica de todos modos. Pero los programadores RTL olvidaron que ya habían ensuciado la matriz original, que ahora consiste en la nueva matriz colocada encima de la anterior: BBBBBBBB BBBBBBBB.

Andreas Rejbrand
fuente
Gracias Andreas! Para estar seguro, usaré punteros para las matrices dinámicas. He probado cómo se manejan los mismos casos con cadenas y funciona como (2), por ejemplo, se asigna una nueva cadena (copia al escribir) y la original (la variable local) no se toca.
dwrbudr
1
@dwrbudr: Personalmente, evitaría usar Deleteen una matriz dinámica. Por un lado, no es barato en matrices grandes (ya que necesariamente necesita copiar una gran cantidad de datos). Y este problema actual me preocupa aún más, obviamente. Pero también esperemos y veamos si los otros miembros de Delphi de la comunidad SO están de acuerdo con mi análisis.
Andreas Rejbrand
1
@dwrbudr: Sí. Por supuesto, los vectores dinámicos no utilizan la semántica copia en escritura, pero los procedimientos gusta SetLength, Inserty Deleteobviamente necesidad de reasignar. Simplemente cambiar un elemento (como b[2] := 4) afectará a cualquier otra variable de matriz dinámica que apunte a la misma matriz dinámica; No habrá copia.
Andreas Rejbrand
1
No use un puntero a un dynarray
David Heffernan
3
@LURD: quality.embarcadero.com/browse/RSP-27870
Andreas Rejbrand