Listas Genéricas, sem Generics


É possível utilizarmos listas genéricas, com checagem de tipo, mas sem o uso da sintaxe com Generics?

Unsplash image Photo by John Murzaku on Unsplash

Muitos programadores adoram utilizar novas features dos compiladores. Uma dessas “novidades” — bem, já faz alguns anos — é o uso de Generics.

Com Generics é possível, por exemplo, termos listas genéricas que podem ser utilizados com qualquer tipo de classe ou interface. Generics possuem uma “checagem estática” de tipo, tanto para armazenamento como para a recuperação dos itens da lista.

Ao invés de termos as “velhas” listas de ponteiros, fazendo casting dos objetos quando acessamos seus itens, hoje, utilizando Generics, podemos utilizar classes de listas já existentes na VCL/LCL para instanciar e manter uma lista de objetos que irão trabalhar com os tipos já definidos no nosso código.

Na declaração de uma variável de uma lista genérica, informamos o tipo dos itens que irão compor a lista e o compilador faz o resto do trabalho.

Esse artigo não irá abordar as vantagens e desvantagens no uso de Generics. Ao invés disso, irei apenas mostrar como simular uma lista genérica utilizando Object Pascal “clássico”.

Por quê isso é relevante?

Existem alguns motivos como, por exemplo, estar trabalhando com código legado numa versão do compilador sem suporte a Generics; almejar uma melhor performance, mesmo utilizando objetos; manter projetos (libs e frameworks) que podem ser utilizados desde os compiladores mais antigos até a versão mais atual.

O problema de termos uma lista genérica, sem o uso de Generics, é o casting que precisaríamos fazer todas as vezes que recuperarmos os itens da lista.

O mesmo casting seria replicado várias vezes, em todos os pontos que fossem necessário recuperar os objetos.

Por exemplo:

uses
  Contnrs;

type
  TFoo = class
    Name: string;
  end;

procedure TForm1.Button1Click(Sender: TObject);
var
  F: TFoo;
  OL: TObjectList;
begin
  OL := TObjectList.Create(True);
  try
    F := TFoo.Create;
    F.Name := 'Bar';
    OL.Add(F);
    ShowMessage(
      (OL.Items[0] as TFoo).Name // <<< casting
    );
  finally
    OL.Free;
  end;
end;

O código acima utiliza a classe TObjectList da unit Contnrs. Essa classe trabalha com itens de objetos. Em seu construtor, um argumento boolean indica que os objetos serão automaticamente liberados quando a lista for destruída.

Funciona. Mas, o inconveniente é termos que fazer o casting na chamada de Items[].

No entanto, é possível não utilizar casting, mesmo sem o uso de Generics. Para tal, vamos utilizar apenas classes simples e sintaxe clássica de Object Pascal.

Primeiro, precisamos codificar nossa própria lista de objetos, sem herdar diretamente de classes de infraestrutura:

type
  TMyObjects = class
  private
    FList: TObjectList;
  public
    constructor Create;
    destructor Destroy; override;
    function Add(Obj: TObject): TMyObjects;
    function Get(Index: Integer): TObject;
    function Count: Integer;
end;

{ TMyObjects }

constructor TMyObjects.Create;
begin
  FList := TObjectList.Create(True);
end;

destructor TMyObjects.Destroy;
begin
  FList.Free;
  inherited;
end;

function TMyObjects.Add(Obj: TObject): TMyObjects;
begin
  Result := Self;
  FList.Add(Obj);
end;

function TMyObjects.Get(Index: Integer): TObject;
begin
  Result := FList.Items[Index];
end;

function TMyObjects.Count: Integer;
begin
  Result := FList.Count;
end;

A classe TMyObjects poderá ser utilizada para armazenar qualquer tipo de instância de TObject. Mas, o inconveniente ainda permanece, pois ao utilizar Get(Index) o casting será necessário.

Segundo, a solução mais simples é utilizarmos herança de classe e reintroduzir o método que recupera os objetos, utilizando uma nova assinatura.

type
  TFooObjects = class(TMyObjects)
  public
    function Get(Index: Integer): TFoo; reintroduce;
  end;

{ TFooObjects }

function TFooObjects.Get(Index: Integer): TFoo;
begin
  Result := inherited Get(Index) as TFoo;
end;

A nova classe TFooObjects tem uma nova assinatura de Get(). Isso quer dizer que quando o desenvolvedor utilizar o code-completion da IDE, apenas essa “versão” do Get(Index: Integer): TFoo será exibida.

Então, podemos alterar o código de teste anterior para utilizar a nova classe:

procedure TForm1.Button1Click(Sender: TObject);
var
  F: TFoo;
  OL: TFooObjects;
begin
  OL := TFooObjects.Create;
  try
    F := TFoo.Create;
    F.Name := 'Bar';
    OL.Add(F);
    ShowMessage(OL.Get(0).Name);
  finally
    OL.Free;
  end;
end;

Sem casting, limpo e mais “elegante” do que utilizar Generics.

A mesma técnica deve ser utilizada para o método Add(Obj) afim de certificarmos que apenas instâncias de TFoo serão armazenadas.

A classe TMyObjects foi codificada apenas uma vez e poderá ser reutilizada como base para várias outras classes.

Entretanto, alguns desenvolvedores poderão sugerir que tivemos que implementar “muito” código para ter a mesma funcionalidade que a sintaxe de Generics já nos dá. Bem, isso depende do ponto de vista. Além dos motivos sugeridos nesse artigo, o não uso de Generics pode fazer muito sentido quando estamos trabalhando na Camada de Negócio, onde o código deve representar as Regras de Negócio do cliente. Uma classe “inchada” com dezenas de métodos de manipulação de itens na lista pode não representar fielmente, por exemplo, uma “lista de produtos” do cliente.

Finalmente, é você quem vai decidir sobre os trade-offs envolvidos no design do seu código. Mas, sempre tenha em mente que nem tudo que é antigo será pior que o mais novo.

Até logo.

Posts Relacionados

  • Memória Segura Utilizando Instâncias de Interfaces

  • 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?