Este artigo é uma tradução do artigo http://blog.marcocantu.com/blog/2018-november-custom-managed-records-delphi.html
Fala ai Radizeiros e Radizeiras, tudo bem com vocês?
Além das declarações de variáveis inline, o Delphi 10.3 vai oferecer outra nova extensão da linguagem Object Pascal, registros gerenciados personalizados.
Ao lado da declaração de variáveis inline, como explicado em meu post recente, a versão 10.3 do Delphi terá outra adição relevante a linguagem Delphi, a saber, a capacidade de definir o código a ser executado quando os registros são inicializados pela primeira vez, descartados da memória e copiados. Antes de chegarmos a isso, porém, deixe-me recapitular alguns elementos-chave dos registros na linguagem Delphi atual.
Registros em versões recentes
Em Modern Object Pascal (do Delphi 2009, se bem me lembro), o construtor de tipo de registro ganhou muitos recursos novos, da capacidade de definir métodos para sobrecarga de operadores. O principal diferenciador do tipo de classe permanece como a memória é alocada. Embora uma variável baseada em classe seja apenas uma referência à memória alocada dinamicamente, uma variável baseada em registro possui uma cópia dos dados reais.
A principal diferença no layout da memória também está relacionada com o fato de os registros não possuírem métodos e heranças virtuais, um princípio fundamental da programação orientada a objetos. No lado positivo, a maneira como os registros são alocados na memória pode ajudar no gerenciamento de memória e torná-lo mais rápido? como a memória está diretamente disponível pulando uma operação extra de alocação e desalocação.
A outra grande diferença que não temos tempo para explorar na íntegra é o que acontece quando você passa um registro como parâmetro para uma função. Por padrão, o compilador faz uma cópia dos dados do registro inteiro. Você pode evitar a cópia passando o registro por referência (var). Isso tem implicações mais profundas do que parece.
Registros padrão e gerenciados
Os registros podem ter campos de qualquer tipo. Quando um registro tem campos simples (não gerenciados), como valores numéricos ou outros valores enumerados, não há muito o que fazer para o compilador e descartar o registro consiste em se livrar da localização da memória ?? e, provavelmente, torná-lo disponível para ser reutilizado mais tarde. Mas se um registro tiver um campo de um tipo gerenciado (em termos de memória) pelo compilador, as coisas ficarão um pouco mais complexas. Um exemplo seria um registro com um campo de string. A string em si é contada por referência, portanto, quando o registro sai do escopo, a string dentro do registro precisa ter sua contagem de referência diminuída, o que pode levar a uma desalocação da memória da string. Portanto, quando você está usando esse registro gerenciado em uma seção do código, o compilador adiciona automaticamente um bloco try-finally em torno desse código.
Registros gerenciados personalizados
Portanto, se os registros gerenciados já existem há muito tempo, o que significa que o Delphi 10.3 adicionará suporte a eles? O que a linguagem fornecerá é um registro gerenciado “personalizado”. Em outras palavras, você poderá declarar um registro com código de inicialização e finalização personalizado, independentemente do tipo de dados de seus campos, e poderá gravar esse código de inicialização e finalização personalizado. Você faria isso adicionando um construtor sem parâmetros ao tipo de registro e um destruidor (você pode ter um sem o outro, se quiser). Aqui está um trecho de código simples:
type TMyRecord = record Value: Integer; constructor Create; destructor Destroy; end;
Você certamente terá que escrever o código desses dois métodos. A enorme diferença entre esse novo construtor e o que estava disponível anteriormente para registros é a chamada automática. Na verdade, se você escrever algo como:
procedure TForm5.btnMyRecordClick(Sender: TObject); var my1: TMyRecord; begin Log (my1.Value.ToString); end;
você acabará invocando o construtor padrão e o destruidor, e você terminará com um bloco try-finally gerado pelo compilador para sua instância de registro gerenciado.
Você também pode invocar explicitamente o construtor padrão do registro, no código como:
myrec := TMyRecord.Create;
Existe, é claro, o risco, dependendo de como você escreve o código, que o construtor padrão é invocado duas vezes, implicitamente pelo compilador e em seu código. No caso de declarações de variáveis embutidas, isso não acontecerá, mas em outros casos poderá acontecer.
O operador de atribuição
Outro novo recurso trazido com registros gerenciados personalizados é a capacidade de executar código personalizado para atribuir um registro a outro. Em vez de copiar os dados inteiros, campo a campo, você pode querer executar tarefas diferentes, manter uma única cópia de alguns dos campos, duplicar um objeto ao qual um registro se refere ou qualquer outra operação personalizada. O novo operador é chamado com a sintaxe: =, mas definido como “Assign”:
type TMyRecord = record Value: Integer; class operator Assign (var Dest: TMyRecord; const [ref] Src: TMyRecord);
A definição do operador deve seguir regras muito precisas, incluindo ter o primeiro parâmetro como parâmetro de referência e o segundo como var ou const passado como referência. Se você não fizer isso, o compilador emitirá mensagens de erro como:
[dcc32 Error] E2617 First parameter of Assign operator must be a var parameter of the container type [dcc32 Hint] H2618 Second parameter of Assign operator must be a const[Ref] or var parameter of the container type
O operador Assign é chamado se você escrever:
var my1, my2: TMyRecord; begin my1.Value := 22; my2 := my1;
O operador de atribuição é usado em conjunto com as operações de atribuição, como a acima, e também se você usar uma atribuição para inicializar uma variável embutida (caso em que o construtor padrão não é chamado):
var my4 := my1;
Copiar Construtor
Há, no entanto, outro cenário interessante, que é o caso de você passar um parâmetro de registro ou desejar criar um novo registro baseado em um existente. Para lidar com cenários semelhantes, você pode definir um construtor Copy, que é um construtor que recebe um registro do mesmo tipo que o parâmetro. Na maioria dos casos, você também deseja um construtor padrão, portanto, é necessário marcá-los com a diretiva de sobrecarga:
TMyRecord = record constructor Create; overload; constructor Create (const mr: TMyRecord); overload;
Se você agora definir um método ou procedimento com um parâmetro de valor regular (não passado por const ou var), quando você invocá-lo, ele usará o construtor Copiar, não uma chamada de atribuição:
procedure ProcessRec (mr: TMyRecord); begin Log (mr.Value.ToString); end;
Observe que você também recebe o destruidor Destroy chamado no final do procedimento. Outra maneira de invocar o construtor de cópia é usar a seguinte declaração de variável in-line:
var my3 := TMyRecord.Create (my1);
Conclusão e Isenção de Responsabilidade
Um caso de uso muito interessante para registros gerenciados é a implementação de ponteiros inteligentes. Eu escrevi um código que compartilharei em um post futuro. O que eu queria fazer hoje era apenas compartilhar o conceito geral.
Essa é outra alteração significativa para a linguagem e abre ainda mais o uso de tipos de registro. Observe, no entanto, que alguns dos detalhes dos registros gerenciados ainda estão sujeitos a alterações até o GA do Delphi 10.3.