terça-feira, 3 de abril de 2007

Desalocando o Objeto: Destroy or Free?

No post anterior, OO em Delphi, eu falei que nunca o programador deveria desalocar o objeto através do método “Destroy”.Há alguns anos, eu estava dando manutenção num sistema quando o gerente do projeto, o qual gerenciou o desenvolvimento deste sistema desde de sua criação, me chamou a atenção sobre um acesso violado que ocorria eventualmente quando um relatório era executado. A princípio percorri o trecho de código envolvido na criação deste relatório, sem achar nenhuma pista. Uma coisa deixava bem claro que realmente havia problema ali, o código era mais enrolado e grudado que miojo mal feito. Era qry.Sql.Add ... pra cá ...qry.SQl.Add ... pra lá,
qry.Parameters.ParamByName(‘CruzCredo’).DataType ..
A experiência ensina: Onde há fumaça, há fogo. Esse código está caótico, pensei, vou ficar horas aqui procurando agulha no palhero. Vamos tentar causar o erro e debuggarr. Descobri rapidamente que o problema acontecia na linha onde o parâmetro da qry era tipado (qry.Parameters.ParamByName(‘CruzCredo’).DataType := ....). Opa, opa!!! Me perguntei: Porque essa qry estaria desalocada se na execução anterior ela estava em memória? Encurtando o assunto, depois algum tempo tentando imaginar a resposta, de repente .... Plin! Lembrei!! Será!?!?!? Alguém chamou um “Destroy” por aqui? Imaginei logo... Mas aonde?? Pois é eu não conseguia achar o maldito Destroy por conta da tamanha, alucinada, zona que estava aquele módulo. Parti pro “Ctrl + F” (find), lá estava o maldito, depois de um except, ou else, náo lembro. Acho que era mais ou menos assim:

.
.
Aqui já tinha caos .....
if (alguma coisa louca, com certeza um flag bizonho) then
begin
.
.
Várias linhas de caos
.
.
Mais caos .....
.
.
end
else
Qry.Destroy;

A primeira vez que era executado não dava problema, mas se a rotina fosse executada novamente, tomava um acesso violado na lata. Por que isso acontece?
O Método Destroy é um metodo extremamente burro, ele simplesmente vai na área de memória em que o objeto está alocado e desaloca, sem fazer nenhum teste, nem limpar a referencia que existe na variável que instanciou o objeto. No caso, a variável “qry”. Ou seja, o “Destroy” destrói o objeto independente de qualquer coisa. Independente se o objeto estiver sido destruído ou não. Se por acaso o objeto não estiver mais em memória vc vai tomar um acesso violado. Visto que, o Destroy executado anteriormente, desalocou o objeto mais não limpou a referencia de memória do objeto( Aliais, isso seria impossível de acontecer)
Então, o processador, a partir da referencia a um endereço de memória que ainda esta na variável (objeto) tenta ir naquele endereço e executar o método, só que quando ele vai lá não encontra nada. Claro, não tem nada mesmo, o objeto já tinha sido destruído antes. Portanto, nunca desaloque um objeto chamando o destructor.

Vou repetir o que tenho falado em outros artigos, em qualquer literatura sobre orientação a objetos, sempre encontramos para definição de “Objetos” a afirmação: Objeto é a instância de uma “Classe”. Isso não agrega muito conhecimento para quem está aprendo OO. Entretanto, julgo muito mais importante para quem já conhece programação uma informação que invariavelmente é omitida. Algo que explique que um Objeto nada mais é do que uma variável cujo tipo é uma classe. Logo, uma variável cujo tipo é uma classe, não é uma variável estática. Como mencionei anteriormente, ela é uma variável de referência. Isso implica em que? Lembra o que é um ponteiro? Pois bem, uma variável de referência é um ponteiro para uma referencia de memória. Por isso, quando usamos o objeto, temos que alocar memória para ele dinamicamente para ele. Conseqüentemente, se alocamos memória teremos, em algum momento, que desalocá-la. O Objeto quando é instanciado, o compilador aloca memória para ele. Por sua vez, o objeto fica com uma referência para o endereço de memória onde ele foi alocado. O método “Destroy” não retira essa referência quando desaloca o objeto. Cheque mate! Aqui é que mora o problema .....

Se você declara uma variável estática, o compilador reserva um espaço para ela, estaticamente, no momento da linkedição. Esse espaço será alocado na memória stack. Por exemplo: Uma variável “VarZn” do tipo Shortint declarada num programa, é uma variável estática. Quando esse programa for executado vai ser alocado para a “VarZn” um espaço de 8 bits. A memória stack é fixa, ela não aumenta de tamanho nem diminui, ela é estática.

var
VarZn: Shortint;


Em contra partida, quando eu tenho uma variável de referência, um ponteiro, não haverá memória previamente reservada para ela. O compilador vai criar uma referencia para ela na memória heap. Isso porque no momento da linkedição não é possível para ele saber a quantidade de memória necessária para alocar. A memória Heap não é estática, ela pode crescer ou diminuir, dinamicamente de dependendo do que necessite o programa em run time.
Portanto, reforçando a idéia, quando estamos trabalhando com objetos estamos alocando memória dinamicamente, conseqüentemente se alocamos, teremos, em algum momento, que desalocá-la.

Existem duas formas corretas de vc fazer isso dependendo do contexto:


Contexto 1 – Eu tenho uma variável de escopo procedural.
Ou seja ela só existirá quando a procedure ou function for executada, vc deve chamar o método “Free”. Exemplo:

(* Exemplo de código 2 – Desalocando um objeto *)
Type
TGmInstrumento = Class
SampleSound: TWaveStream;
Playng: Boolean;
procedure Play;
end;
.
.
.
Var
GmViolao: TGmInstrumento; // Declaração da variável objeto
begin
GmViolao := TGmInstrumento.Create;
Try // bloco protegido, caso aconteça uma exceção quando Play for executado
GmViolao.Play;
Finally // garante que o Free seja executado
GmViolao.Free; // desaloca o objeto
End;
End;

OBS:
A estrutura try ... finally é uma forma de protergermos um trecho de código.

O Free é um método um pouco mais inteligente. Ele chama o descructor, só que antes disso ele verifica de o objeto está instanciado. Veja o código retirado da definição da classe TObject:


procedure TObject.Free;
begin
if Self <> nil then
Destroy;
end;


Na verdade ele não testa se existe a referencia de memória, ele testa se o ponteiro ainda esta atribuído. O que não ajuda muito se o objeto estiver com o ponteiro atribuído mas não está em memória. Vc leva acesso violado de novo. Isso é um perigo real e eminente. Por exemplo:

(* Exemplo de código 3 –  Desalocando um objeto*)
Type
TGmInstrumento = Class
Playng: Boolean;
end;
.
.
.
procedure TForm1.Button1Click(Sender: TObject);
const
msgRef = 'Referência de memória = %d';
var
Violao: TGmInstrumento;
begin
Violao := TGmInstrumento.Create;
Violao.Playng := True;
Violao.Destroy;

if Assigned(Violao) then
ShowMessage(Format(msgRef, [integer(Violao)]));// Veja a referência de memória
if Violao <> nil then // verifica se o ponteiro ainda esta atribuído
Violao.Free;
end;


Quando você executar esse código vai levar um acesso violado, confirmando o que eu expliquei no parágrafo anterior. Logo, mesmo que vc ao invés de chamar o “Destroy” na linha 17 chame o “Free”, não muda nada em termos práticos, vai levar acesso violado da mesma forma. Se de forma diferente você ao desalocar o objeto retirar a referência de memória, atribuindo “nil” a variável, o problema estará resolvido.

(* Exemplo de código 4 – Desalocando um objeto de maneira correta*)
Type
TGmInstrumento = Class
Playng: Boolean;
end;
.
.
.
procedure TForm1.Button1Click(Sender: TObject);
const
msgRef = 'Referência de memória = %d';
var
ZnViolao: TGmInstrumento;
begin
ZnViolao := TGmInstrumento.Create;
ZnViolao.Playng := True;
ZnViolao.Free;
ShowMessage(Format(msgRef, [integer(ZnViolao)]));// Veja a referência de memória
(*Limpando a referência de memória*)
ZnViolao := nil;
ShowMessage(Format('Limpei a referência - ' + msgRef,
[integer(ZnViolao)]));// Veja a referência de memória
if ZnViolao <> nil then // verifica se o ponteiro ainda esta atribuído
ZnViolao.Free;
end;

Agora vc pode executar o evento OnClick do botão 1 mil vezes que nada de errado vai acontecer.

OBS:
A função “Assigned” determina quando um ponteiro ou procedure são nullos. Por exemplo:


var
ZnPointer: Pointer;
begin

if Assigned(ZnPointer) then
ShowMessage('Belng!!!');


É exatamente a mesma coisa que:

var
ZnPointer: Pointer;
begin

if @ ZnPointer <> nil then
ShowMessage('Belng!!!');




Entretanto para saber se o ponteiro aponta para uma referência de memória válida, ou seja alocada , ele é ineficiente. Veja o trecho de código abaixo:

procedure TForm1.BtnTesteAssignedClick(Sender: TObject);
var
ZnPointer: Pointer;
begin

ZnPointer := nil;
ShowMessage('Veja que o ponteiro está nulo, mas ainda assim possui uma referência de memória');
ShowMessage('ZnPointer está nulo - ' + IntToStr(Integer(ZnPointer)));
ShowMessage('@ZnPointer mostra sua referência de memória - ' + IntToStr(Integer(@ZnPointer)));

if @ZnPointer <> nil then
ShowMessage(Format('Belng 1!!! confirmo que a referência existe: %d', [Integer(@ZnPointer)]));

if ZnPointer <> nil then
ShowMessage('Bla 1!!! Se Bla 1 for exibido o ponteiro não está nullo');

if Addr(ZnPointer) <> nil then
ShowMessage('O ponteiro possui uma referencia');

if Assigned(ZnPointer) then
ShowMessage('A função "Assigned" não consegue identificar se existe ou não referência de memória no ponteiro')
else
ShowMessage('A função "Assigned" apenas identifica que o ponteiro está nulo');

GetMem(ZnPointer, 1024); {Alocando memoria}

if @ZnPointer <> nil then
ShowMessage('Belng 2!!! Acabei de alocar memória, nada muda no que refere a referência');

if ZnPointer <> nil then
ShowMessage(Format('Bla 2!!! Acabei de alocar memória, por isso o ' +
' ponteiro não está nulo. ZnPointer = %d ', [Integer(ZnPointer)]));

ShowMessage(IntToStr(Integer(@ZnPointer)));

{ Desalocando memória, mas o ponteiro não ficará nulo,
muito menos a referência foi alterada }
FreeMem(ZnPointer, 1024);

ShowMessage('Liberei a memória alocada. Contudo, o ponteiro não está nulo. '+
'muito menos a referência foi sequer alterada');

if Assigned(ZnPointer) then
ShowMessage(Format('Você esta vendo que o "Assiged" não é eficiente para notar ' +
' que o memória alocada para o ponteiro foi liberada. ZnPointer = %d', [Integer(ZnPointer)]));

if @ZnPointer <> nil then
ShowMessage('Belng!!! testando @ZnPointer');

if ZnPointer <> nil then
ShowMessage('Bla!!! Embora tenha liberado a memória alocada anteriormente, ' +
'note que o ponteiro não está nulo');

ZnPointer := nil;
ShowMessage(Format('Agora atribui nulo ao ponteiro. ZnPointer = %d; endereço = %d', [Integer(ZnPointer), Integer(Addr(ZnPointer))]));

end;

Adicionei um BitBtn num TForm qualquer, em seguida alterei a propriedade "Name" dele para "BtnTesteAssigned" e codifiquei.
Note na linha 21, a função "Addr" retorna um endereço de um ponteiro.
Contexto 2 – Eu tenho uma variável de escopo Global na unit.
Pelo amor de Deus, esse tipo de visibilidade de variável é algo de deve ser evitado a todo custo. Fujam de caírem na desgraça das variáveis globais. Mas, nem sempre isso é possível.

(* Exemplo de código 5 – Desalocando um objeto *)
Type
TGmInstrumento = Class
Playng: Boolean;
end;
.
.
.

var
Form1: TForm1;
GmViolao: TGmInstrumento; // Declaração da variável objeto

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
Violao := TGmInstrumento.Create;
try
Playng := True;
finally
FreeAndNil(Violao);
end.
end;

A procedure “FreeAndNil” faz o trablho completo. Veja o Código retirado do Delphi:

procedure FreeAndNil(var Obj);
var
Temp: TObject;
begin
Temp := TObject(Obj);
Pointer(Obj) := nil;
Temp.Free;
end;

Ela recebe como parâmetro um endereço de memória, faz um cast para poder atribuir o conteúdo neste endereço para um objeto. Retira a referencia, limpa o ponteiro atribuindo “nil” para ele. Em seguida chama o método Free. Como “Temp” é uma variável de escopo procedural ela deixa de existir assim que a rotina terminar de ser executada.

2 comentários:

  1. um otimo artigo ( post ), muito bem explicado e com exemplos bem claros , vc está de parabêns ... apesar de eu ter achado esse post e procura de outra duvida que não foi posivel eu conseguir retirar aki ... isso me atribuiu outros conhecimentos e experiencia par resolver outros problemas que eu vinah tendo e num fazia ideia do que era ! valew mesmo....

    ass: jeter , msn :jetercampos@hotmail.com

    ResponderExcluir
  2. Amigão, gostei muito, mas muito mesmo este post e já me direcionou para outros erros que eu vinha cometendo e não percebia. Como o amigo acima mencionou, eu tb estava a procura de outra coisa e esta tb me foi muito útil, mas deixe eu perguntar por favor: Tenho meu servidor que sofre chamadas RPC a cada 0,5 segundos e não estou conseguindo fazer com que de um certo tempo, tipo de 10 em 10 minutos limpar as threads de memória, para evitar assim gargalos e o RPC não causar travamentos ou mesmo lentidão. Teria alguma idéia de como eu poderia resolver isso? Grato. Se puder ajudar agradeceria muito.

    ResponderExcluir

 
BlogBlogs.Com.Br