Memória Segura Utilizando Instâncias de Interfaces


Você sabe que ao utilizar instâncias de interfaces não é necessário destruir o objeto manualmente, utilizando o método Free.

Isso ocorre por quê instâncias de interfaces possuem uma contagem de referência e, assim, o compilador sabe quando desalocar a instância automaticamente.

Mas e se se ocorrer uma exceção dentro de um método de uma instância, numa composição de instâncias de interfaces, seria seguro não utilizar try-finally para ter a certeza que todos os objetos serão desalocados, atribuindo nil as instâncias?

Unsplash image
Photo by Gabriel Gurrola on Unsplash

Introdução

Na semana passada eu recebi um email de um leitor do blog, o Carlos, e ele me questionou sobre ter ou não segurança na desalocação apropriada de memória quando utilizamos instâncias de interfaces.

A dúvida dele é interessante.

Segundo Carlos, utilizar instancias de interfaces seria inseguro em comparação com instâncias de classes, pois “se uma exceção ocorrer dentro de algum método, você não terá a chance de desalocar as variáveis de interfaces como fazemos com o try-finally e instâncias de classes” — ele escreveu.

Então ele me enviou um projeto de teste que “provaria” que instâncias de interfaces poderiam ser inseguras e que não há vantagem em relação às instâncias de classes no quesito desalocação de memória pois, segundo ele, seríamos obrigados a utilizar o try-finally para estarmos “100% seguro”.

O exemplo era um pouco mais complexo do que deveria, então eu o reescrevi e enviei à ele. Ele concordou que ficou mais simples de entender mas, o novo exemplo ainda demonstrava que ele estava certo — ele afirmou — pois ainda haviam os vazamentos de memória no final da execução do programa.

Mas até aquele momento, eu só queria ter uma base de código mais simples para, então, mostrar-lhe uma segunda opinião.

Código

O código original foi reescrito, porém a nova versão contem todos os “problemas” apontados por ele e, consequentemente, a mesma saída no final da execução.

Esse é o código completo:

program Project1;

{$mode delphi}

uses
  SysUtils, Classes;

type
  IFoo = interface
    procedure Execute;
  end;

  IBar = interface
    procedure Execute;
  end;

  TFoo = class(TInterfacedObject, IFoo)
  private
    fBar: IBar;
  public
    constructor Create(const aBar: IBar);
    procedure Execute;
  end;

  TBar = class(TInterfacedObject, IBar)
  public
    procedure Execute;
  end;

{ TFoo }

constructor TFoo.Create(const aBar: IBar);
begin
  inherited Create;
  fBar := aBar;
end;

procedure TFoo.Execute;
begin
  fBar.Execute;
end;

{ TBar }

procedure TBar.Execute;
begin
  raise Exception.Create('exception!!!');
end;

var
  foo: IFoo;
begin
  foo := TFoo.Create(TBar.Create);
  foo.Execute;
end.

É um aplicativo de linha-de-comando.

Crie um novo projeto e não se esqueça de marcar a opção para ver o resultado da memória — se você estiver utilizando Lazarus, marque a opção “Use Heaptrc…” em Project Options, debugging.

O término do programa deverá ser algo parecido com isso:

> project1.exe
An unhandled exception occurred at $004017DA:
Exception: exception!!!
  $004017DA  TBAR__EXECUTE,  line 46 of W:/md/dev/proj/test/ide/projects/project1.lpr
  $004017C8  TFOO__EXECUTE,  line 40 of W:/md/dev/proj/test/ide/projects/project1.lpr
  $004018D2  main,  line 54 of W:/md/dev/proj/test/ide/projects/project1.lpr

Heap dump by heaptrc unit of W:\md\dev\proj\test\ide\projects\project1.exe
74 memory blocks allocated : 2075/2232
71 memory blocks freed     : 1975/2128
3 unfreed memory blocks : 100
True heap size : 262144 (112 used in System startup)
True free heap : 261600
Should be : 261640
Call trace for block $014D7628 size 64
  $0040A1AC
  $004017FA  TBAR__EXECUTE,  line 47 of W:/md/dev/proj/test/ide/projects/project1.lpr
  $004017C8  TFOO__EXECUTE,  line 40 of W:/md/dev/proj/test/ide/projects/project1.lpr
  $004018D2  main,  line 54 of W:/md/dev/proj/test/ide/projects/project1.lpr
Call trace for block $014A8D70 size 24
  $0040A1AC
  $004017FA  TBAR__EXECUTE,  line 47 of W:/md/dev/proj/test/ide/projects/project1.lpr
  $004017C8  TFOO__EXECUTE,  line 40 of W:/md/dev/proj/test/ide/projects/project1.lpr
  $004018D2  main,  line 54 of W:/md/dev/proj/test/ide/projects/project1.lpr
Call trace for block $014A8CF0 size 12
  $004017EE  TBAR__EXECUTE,  line 47 of W:/md/dev/proj/test/ide/projects/project1.lpr
  $004017C8  TFOO__EXECUTE,  line 40 of W:/md/dev/proj/test/ide/projects/project1.lpr
  $004018D2  main,  line 54 of W:/md/dev/proj/test/ide/projects/project1.lpr

Não importando as diferenças na finalização do programa entre Lazarus e Delphi, apenas tenha atenção nessa linha:

3 unfreed memory blocks

O código não tem nenhum try-finally e estamos utilizando instâncias de interfaces, como IFoo e IBar, e a saída são 3 blocos de memória não liberados, devido ter ocorrido uma exceção em TBar.Execute. Do contrário, não haveria nenhum vazamento de memória — você pode verificar isso comentando a linha que gera a exceção.

Isso prova que interfaces são inseguras se exceções ocorrem, certo?

Bem, não tão rápido.

Primeiro de tudo, sugiro que você leia o código, linha a linha, e veja se consegue descobrir o que há de errado.

O Diabo Mora nos Detalhes

Para começar, o vazamento de memória acima não tem nada haver com instâncias de interfaces. Na verdade, você pode remover as interfaces completamente, adicionar try-finally para desalocar a memória manualmente e, mesmo assim, a saída do Heaptrc irá continuar a mesma.

Não acredita?

Tente você:

program Project1;

{$mode delphi}

uses
  SysUtils, Classes;

type
  TBar = class;

  TFoo = class
  private
    fBar: TBar;
  public
    constructor Create(const aBar: TBar);
    procedure Execute;
  end;

  TBar = class
  public
    procedure Execute;
  end;

{ TFoo }

constructor TFoo.Create(const aBar: TBar);
begin
  inherited Create;
  fBar := aBar;
end;

procedure TFoo.Execute;
begin
  fBar.Execute;
end;

{ TBar }

procedure TBar.Execute;
begin
  raise Exception.Create('exception!!!');
end;

var
  bar: TBar;
  foo: TFoo;
begin
  bar := TBar.Create;
  foo := TFoo.Create(bar);
  try
    foo.Execute;
  finally
    bar.Free;
    foo.Free;
  end;
end.

Se você compilou e executou essa versão, viu que a saída do programa com o relatório do Heaptrc (ou similar) é praticamente o mesmo.

Os 3 vazamentos de memória continuam, provando que o “problema” não são as interfaces.

Será um problema no compilador?

Solução

Primeiramente, vamos separar o código principal em uma unidade para ser utilizada tanto em uma aplicação Console como GUI.

Assim vou conseguir demostrar um ponto importante, futuramente.

Aqui está o código refatorado, ou seja, apenas as interfaces e classes separadas em uma nova unidade chamada MyClasses.

unit MyClasses;

{$mode delphi}

interface

uses
  SysUtils, Classes;

type
  IFoo = interface
    procedure Execute;
  end;

  IBar = interface
    procedure Execute;
  end;

  TFoo = class(TInterfacedObject, IFoo)
  private
    fBar: IBar;
  public
    constructor Create(const aBar: IBar);
    procedure Execute;
  end;

  TBar = class(TInterfacedObject, IBar)
  public
    procedure Execute;
  end;

implementation

{ TFoo }

constructor TFoo.Create(const aBar: IBar);
begin
  inherited Create;
  fBar := aBar;
end;

procedure TFoo.Execute;
begin
  fBar.Execute;
end;

{ TBar }

procedure TBar.Execute;
begin
  raise Exception.Create('exception!!!');
end;

end.

Agora, crie um novo projeto GUI. Adicione um Form. Depois um botão nesse Form.

Novamente, se você estiver utilizando Lazarus, marque a opção “Use Heaptrc…” em Project Options, debugging.

Adicione MyClasses ao projeto e ao uses do Form.

No click do botão, codifique:

procedure TForm1.Button1Click(Sender: TObject);
var
  foo: IFoo;
begin
  foo := TFoo.Create(TBar.Create);
  foo.Execute;
end;

Ao executar o projeto e clicar no botão, você verá a mensagem de exceção:

Project project1 raised exception class 'Exception' with message: exception!!!
In file 'MyClasses.pas' at line 51

Clique em continuar para ver a verdadeira exceção e depois em OK.

Finalmente, feche o programa.

Se você não esqueceu de marcar o Heaptrc, um ShowMessage será mostrado:

Heap dump by heaptrc unit of W:\md\dev\proj\test\ide\projects\project1.exe
1764 memory blocks allocated : 1804912/1810440
1764 memory blocks freed     : 1804912/1810440
0 unfreed memory blocks : 0
True heap size : 688128 (112 used in System startup)
True free heap : 688016

Nenhum vazamento de memória, utilizando o mesmo código do início do artigo.

O que está acontecendo aqui?

O verdadeiro problema é não capturar a exceção em nenhum lugar do programa.

Mas um programa GUI, por padrão, captura todas as exceções caso o programador não o faça deliberadamente.

E isso faz com que o programa continue em execução “mais ou menos” no ponto onde a exceção foi gerada, ou seja, o programa continua em execução.

E aqui entra uma vantagem das instâncias de interfaces: o compilador adiciona um try-finally implícito para que suas contagens de referência sejam decrementadas.

Sabemos que um try-finally sempre será executado, independentemente de houve uma exceção dentro do seu bloco.

Por isso as instâncias de IFoo e IBar são decrementadas e, consequentemente, destruídas mesmo após ter ocorrido uma exceção.

Vamos voltar ao programa de linha-de-comando.

Crie um novo programa e adicione a unidade MyClasses.

O código é similar ao código do botão, porém é necessário um try-except, pois aplicações console (por padrão) não tem um handle de exceções:

program Project1;

{$mode delphi}

uses
  SysUtils, Classes, MyClasses;

var
  foo: IFoo;
begin
  foo := TFoo.Create(TBar.Create);
  try
    foo.Execute;
  except
    on e: Exception do
       writeln(e.Message);
  end;
end.

Compile e execute.

Veja que não há mais vazamentos de memória.

> project1.exe
exception!!!
Heap dump by heaptrc unit of W:/md/dev/proj/test/ide/projects/project1.exe
72 memory blocks allocated : 1916/2064
72 memory blocks freed     : 1916/2064
0 unfreed memory blocks : 0
True heap size : 196608 (112 used in System startup)
True free heap : 196496

Mesmo sem o try-finally as instâncias de IBar e IFoo foram destruídas automaticamente pelo compilador.

Conclusão

A utilização de instâncias de interface é, de certa forma, mais segura do que instância de classes.

No entanto, se você começar a brincar com ponteiros, cast entre instâncias de classe vs interface, referência circular, etc, sem saber o que está fazendo, será grande a chance de haver vazamentos de memória.

Porém, utilizando o código padrão para instanciar objetos do tipo interface, seu código estará mais seguro contra vazamentos de memória, pois o compilador sempre irá desalocar as instâncias.

Até logo.

Posts Relacionados

  • Classes Mutáveis vs Objetos Imutáveis

  • Implementando Interfaces Utilizando Diferente Assinaturas de Métodos

  • Usando Paths ao invés de Diretivas de Compilação

  • Trabalhando com Exceções em Requisições HTTP

  • Tipo object Continua Vivo

  • Array de Objetos

  • Variáveis Locais Deveriam ter Nomes Curtos

  • Como Dividir e Organizar o Código em Formulários com Muitos Widgets

  • Pascal Deveria ser Modernizado?

  • Records - Antiga Nova Tecnologia