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 UpdateArray
método exclui o primeiro elemento da matriz. Mas o comprimento da matriz permanece 2. O UpdateArrayWithParam
mé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;
Delete
procedimento. 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.Respostas:
Esta é uma pergunta interessante!
Desde que
Delete
altera o comprimento da matriz dinâmica - assim comoSetLength
faz - 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
Delete
será 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
SetLength
função semelhante :Mas, surpreendentemente, isso não acontece exatamente neste caso.
Considere este exemplo mínimo:
Eu escolhi os valores para que sejam fáceis de localizar na memória (Alt + Ctrl + E).
Após (1),
a
aponte para$02A2C198
minha execução de teste: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:Como esperado, a contagem de referência agora é 3.
Agora, vamos ver o que acontece depois (3).
a
agora aponta para uma nova matriz dinâmica2A30F88
no meu teste: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
b
ainda está apontando, parecesse como antes, mas com uma contagem de referência reduzida de 2. Mas parece com isso agora: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
Delete
procedimento 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
Delete
procedimento não menciona isso.Parece um bug.
Atualização: o código RTL
Eu dei uma olhada no código fonte do
Delete
procedimento, e isso é bastante interessante.Pode ser útil comparar o comportamento com o de
SetLength
(porque esse funciona corretamente):Se a contagem de referência da matriz dinâmica for 1,
SetLength
tente simplesmente redimensionar o objeto de heap (e atualizar o campo de comprimento da matriz dinâmica).Caso contrário,
SetLength
cria 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
Delete
sempre reduz a matriz, é tentador tentar simplesmente reduzir o tamanho do objeto de heap onde está. E é realmente isso que o código RTL tentaSystem._DynArrayDelete
. Portanto, no seu caso, oBBBBBBBB
é movido para o início da matriz. Tudo está bem.Mas então chama
System.DynArraySetLength
, que também é usado porSetLength
. E este procedimento contém o seguinte comentário,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
.fonte
Delete
em 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.SetLength
,Insert
eDelete
, obviamente, precisa realocar. Apenas alterar um elemento (comob[2] := 4
) afetará qualquer outra variável de matriz dinâmica que aponte para a mesma matriz dinâmica; não haverá cópia.