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 ..
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.
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.
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....
ResponderExcluirass: jeter , msn :jetercampos@hotmail.com
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