Esse comportamento do array dinâmico Delphi é esperado

8

A questão é: como matrizes dinâmicas são gerenciadas internamente pelo Delphi quando são definidas como um membro da classe? Eles são copiados ou passados ​​por referência? Delphi 10.3.3 usado.

O UpdateArraymétodo exclui o primeiro elemento da matriz. Mas o comprimento da matriz permanece 2. O UpdateArrayWithParammétodo também exclui o primeiro elemento da matriz. Mas o comprimento da matriz é corretamente reduzido para 1.

Aqui está um exemplo 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
fonte
1
Matrizes dinâmicas são passadas por referência. Eles são contados por referência e gerenciados pelo compilador. A documentação é muito boa. (E os detalhes .)
Andreas Rejbrand
Então, por que o comprimento da matriz não é atualizado depois que o UpdateArray é chamado?
dwrbudr 12/03
Não, não é. Depois que recUpdate.UpdateArray é chamado, Length (lArr) é 2
dwrbudr
É por causa do Deleteprocedimento. Ele precisa realocar a matriz dinâmica e, portanto, todos os ponteiros para ela "precisam" se mover. Mas ele conhece apenas um desses indicadores, a saber, o que você fornece a ele.
Andreas Rejbrand 12/03
Analisei o problema e devo admitir que sou incapaz de explicá-lo. Parece um bug. Mas talvez David, Remy ou outra pessoa saiba mais sobre isso do que eu.
Andreas Rejbrand 12/03

Respostas:

8

Esta é uma pergunta interessante!

Desde que Deletealtera o comprimento da matriz dinâmica - assim como SetLengthfaz - ele precisa realocar a matriz dinâmica. E também altera o ponteiro dado a esse novo local na memória. Mas, obviamente, ele não pode alterar nenhum outro ponteiro para o antigo array dinâmico.

Portanto, ele deve diminuir a contagem de referência da antiga matriz dinâmica e criar uma nova matriz dinâmica com uma contagem de referência de 1. O ponteiro fornecido Deleteserá definido para essa nova matriz dinâmica.

Portanto, o antigo array dinâmico deve ser intocado (exceto pela contagem reduzida de referências, é claro). Isso está essencialmente documentado para a SetLengthfunção semelhante :

Após uma chamada para SetLength, Sé garantido que faça referência a uma string ou matriz exclusiva - ou seja, uma string ou matriz com uma contagem de referência de uma.

Mas, surpreendentemente, isso não acontece exatamente neste caso.

Considere este exemplo 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;

Eu escolhi os valores para que sejam fáceis de localizar na memória (Alt + Ctrl + E).

Após (1), aaponte para $02A2C198minha execução de teste:

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

Aqui, a contagem de referência é 2 e o comprimento da matriz é 2, conforme o esperado. (Consulte a documentação para o formato de dados interno para matrizes dinâmicas.)

Depois de (2) a = b, isto é Pointer(a) = Pointer(b),. Ambos apontam para a mesma matriz dinâmica, que agora se parece com isso:

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

Como esperado, a contagem de referência agora é 3.

Agora, vamos ver o que acontece depois (3). aagora aponta para uma nova matriz dinâmica 2A30F88no meu teste:

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

Como esperado, esse novo array dinâmico possui uma contagem de referência de 1 e apenas o "elemento B".

Eu esperaria que a antiga matriz dinâmica, que bainda está apontando, parecesse como antes, mas com uma contagem de referência reduzida de 2. Mas parece com isso agora:

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

Embora a contagem de referência seja realmente reduzida para 2, o primeiro elemento foi alterado.

Minha conclusão é que

(1) Faz parte do contrato do Deleteprocedimento que ele invalida todas as outras referências à matriz dinâmica inicial.

ou

(2) Deve se comportar como descrevi acima, caso em que isso é um bug.

Infelizmente, a documentação para o Deleteprocedimento não menciona isso.

Parece um bug.

Atualização: o código RTL

Eu dei uma olhada no código fonte do Deleteprocedimento, e isso é bastante interessante.

Pode ser útil comparar o comportamento com o de SetLength(porque esse funciona corretamente):

  1. Se a contagem de referência da matriz dinâmica for 1, SetLengthtente simplesmente redimensionar o objeto de heap (e atualizar o campo de comprimento da matriz dinâmica).

  2. Caso contrário, SetLengthcria uma nova alocação de heap para uma nova matriz dinâmica com uma contagem de referência de 1. A contagem de referência da matriz antiga é reduzida em 1.

Dessa forma, é garantido que a contagem de referência final seja sempre 1- ou foi desde o início ou uma nova matriz foi criada. (É bom que você nem sempre faça uma nova alocação de heap. Por exemplo, se você tiver uma matriz grande com uma contagem de referência 1, simplesmente truncá-la é mais barato do que copiá-la para um novo local.)

Agora, como Deletesempre reduz a matriz, é tentador tentar simplesmente reduzir o tamanho do objeto de heap onde está. E é realmente isso que o código RTL tenta System._DynArrayDelete. Portanto, no seu caso, o BBBBBBBBé movido para o início da matriz. Tudo está bem.

Mas então chama System.DynArraySetLength, que também é usado por SetLength. E este procedimento contém o seguinte comentário,

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

antes de detectar que o objeto é realmente compartilhado (no nosso caso, ref count = 3), faz uma nova alocação de heap para uma nova matriz dinâmica e copia a antiga (reduzida) para esse novo local. Reduz a contagem de ref da matriz antiga e atualiza a contagem de ref, o comprimento e o ponteiro de argumento da nova.

Então, acabamos com uma nova matriz dinâmica de qualquer maneira. Mas os programadores RTL esqueceu que já tinha foi cancelada a matriz original, que agora consiste na nova matriz colocada em cima do antigo: BBBBBBBB BBBBBBBB.

Andreas Rejbrand
fonte
Obrigado Andreas! Para garantir a segurança, usarei ponteiros para matrizes dinâmicas. Eu testei como os mesmos casos são tratados com strings e funcionam como (2), por exemplo, uma nova string é alocada (cópia na gravação) e a original (a variável local) é intocada.
dwrbudr 12/03
1
@dwrbudr: Pessoalmente, eu evitaria usar Deleteem uma matriz dinâmica. Por um lado, não é barato em matrizes grandes (já que é necessário necessariamente copiar muitos dados). E essa questão atual me deixa ainda mais preocupado, obviamente. Mas vamos esperar também e ver se os outros membros da comunidade SO da Delphi concordam com a minha análise.
Andreas Rejbrand 12/03
1
@dwrbudr: Sim. Claro, matrizes dinâmicas não use semântica copy-on-write, mas procedimentos gosto SetLength, Inserte Delete, obviamente, precisa realocar. Apenas alterar um elemento (como b[2] := 4) afetará qualquer outra variável de matriz dinâmica que aponte para a mesma matriz dinâmica; não haverá cópia.
Andreas Rejbrand 12/03
1
Não use um ponteiro para um dynarray
David Heffernan
3
@LURD: quality.embarcadero.com/browse/RSP-27870
Andreas Rejbrand