domingo, 11 de julho de 2010

Segredos do ForEach

Você sabe como funciona e como utilizar o ForEach no Delphi?
O suporte ao ForEach foi adicionado no Delphi 2005 e tem como finalidade oferecer uma interface de acesso aos itens das coleções (listas, filhas, filhas, array, conjuntos, vetores, ...) de forma simplificada e sem se preocupar com contadores ou com índices.

A sintaxe do ForEach é:
for item in collection do
begin
  //
end;

Note que na sitaxe do comando aparece a palavara reservada "in", o que reforça a idéia de um laço de repetição destinado a trabalhar com conjuntos/coleções, e é exatamente essa idéia que se deve manter em mente ao utilizar esse recurso em seu código fonte.
Dependendo do tipo de coleção que se está trabalhando, o comando terá uma característica específica, mas sempre mantendo a finalidade original.

Vamos ver alguns exemplos de uso.

1. Uso em conjuntos
Esse é um dos usos mais freqüentemente encontrado nos códigos fontes e que particularmente encorajo sua adoção e emprego.
Para quem não está muito familiarizado com o uso de conjuntos, o exemplo abaixo pode parecer estranho, num primeiro momento, mas numa segunda olhada verá que é muito intuitivo.

type
  TWeekDay = (wdDomingo, wdSegunda, wdTerca, wdQuarta, wdQuinta, wdSexta, wdSabado);
  TWeek = set of TWeekDay;
const
  STRWEEKDAY: array[TWeekDay] of string =
  ('Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sab');
var
  lDay: TWeekDay;
  lWorkDays: TWeek;
begin
  lWorkDays := [wdSegunda, wdQuarta, wdSexta];

  for lDay in lWorkDays do
  begin
    ShowMessage(STRWEEKDAY[lDay]);
  end;
end;

O exemplo acima define o conjunto TWeek baseado nos dias da semana (TWeekDay) e usa essa definição para declarar a variável local lWorkDays.
Na primeira linha do bloco, nosso conjunto lWorkDays recebe a atribuição de três elementos, ou melhor, de três dias da semana e é utilizada no ForEach abaixo.
Como a coleção aqui é um conjunto, o laço de repetição do comando for ir percorrer todas as possíveis entradas do conjunto, mas somente executará as instruções do bloco quando a respectiva entrada realmente está presente no conjunto. Assim, no nosso exemplo, a função ShowMessage será chamada três vezes.
Reforço a vantagem do uso do ForEach com conjuntos porque além de intuitivo o código binário gerado é limpo, rápido e não consome recursos como o quando é utilizado com objetos.

2. Uso com strings
A utilização do ForEach com strings é um exemplo clássico do passado, mas que ao meu ver tem mais pontos negativos do que positivos.

const
  S: string = 'Using ForEach with strings';
var
  lChar: Char;
begin
  for lChar in S do
  begin
    if lChar in ['a','e','i','o','u'] then
    begin
      // ...
    end;
  end;
end;

Como dito anteriormente, o comando terá uma particularidade que depende do tipo de coleção utilizada, mas sempre centrado na idéia de acessar os elementos da coleção.
No exemplo acima nossa coleção abrange todo o espectro do tipo Char, nativo do Delphi, e o laço de repetição executará tantas vezes quanto for o número de caracteres da string.
Em outras palavras o comando for irá percorrer toda a string, atribuindo para a variável local lChar o valor de cada caracter.
Eu desencorajo a adoção dessa prática como alternativa para percorrer strings porque o código binário gerado é, do ponto de vista de micro-otimizações, bastante oneroso por requerer a manipulação do tipo string.

3. Uso com arrays estáticos
Também podemos utilizar o laço de repetição para percorrermos arrays de forma muito semelhante ao exemplo acima.

type
  TMonthDays = array[1..31] of Boolean;
var
  lWorkedDays: TMonthDays;
  lDay: Boolean;
  lCounter: Integer;
begin
  FillMemory(@lWorkedDays, SizeOf(lWorkedDays), 0);

  lWorkedDays[1] := True;
  lWorkedDays[2] := True;
  lWorkedDays[5] := True;

  lCounter := 0;
  for lDay in lWorkedDays do
  begin
    if lDay then
      Inc(lCounter);
  end;
  ShowMessage('Sum: ' + IntToStr(lCounter));
end;

Observe que nesse nosso exemplo o índice do array não é utilizado, mas apenas o valor de cada entrada.
Aqui temos uma situação que merece um certo destaque por permitir ao código fonte um incremento na clareza e legibilidade, evitando a declaração de variáveis locais e especificando explicitamente os limites do array.
Se considerarmos uma abordagem mais clássica usando o laço for comum como, por exemplo:
var
  i: Integer;
begin
  // ...
  for i = 1 to 31 do
    // ...
corremos o risco de introduzir um bug no sistema caso os limites do array seja moificado.
Obviamente podemos evitar esse tipo de bug removendo os hard codes,
  for i = Low(TMonthDays) to High(TMonthDays) do
mas ainda assim dependeríamos do uso de uma variável como contador do laço de repetição.

4. Uso com arrays dinâmicos
Essa é uma situação levemente diferente do exemplo acima onde, em vez de termos um array com limites bem definidos, temos um array dinâmico cujo tamanho pode variar inclusive durante a execução do ForEach.
Aqui novamente estamos considerando que saber o índice atual do laço de repetição não é requerido, mas somente o valor de cada entrada do array.

var
  lWorkedDays: array of Boolean;
  lDay: Boolean;
  lCounter: Integer;
begin
  SetLength(lWorkedDays, 31);

  lWorkedDays[0] := True;
  lWorkedDays[1] := True;
  lWorkedDays[4] := True;

  lCounter := 0;
  for lDay in lWorkedDays do
  begin
    if lDay then
      Inc(lCounter);
  end;
  ShowMessage('Sum: ' + IntToStr(lCounter));
end;

Uma particularidade do array dinâmico é que a primeira entrada tem índice zero (0).
Utilizando o ForEach para percorrer o array, seja estático ou dinâmico, o limite inicial e final é tratado de forma transparente pelo compilador.

5. Usando com objetos
Esse é o último caso de uso do ForEach apresentado nesse artigo e o que merece mais atenção.
Como os tipos nativos (conjuntos, strings e arrays) o Delphi tem total conhecimento de como devem ser manipulados. Já quando a coleção é uma objeto, essa premissa não é verdadeira e, nesse caso, é você quem deve instruir o Delphi a acessar os itens do objeto de forma adequada.

Observe o exemplo abaixo.
var
  lFiles: TStrings;
  lFilename: string;
begin
  // ...
  for lFilename in lFiles do
  begin
    // ...
  end;
end;

No código acima a variável lFiles está sendo usada para armazenar uma lista de nomes de arquivos que serão tratados, e o laço de repetição irá acessar cada item dessa lista e repassar à variável local lFilename o valor do item (nesse caso, a string com o nome do arquivo).
O mesmo caso é válido para outras classes nativas do Delphi como por exemplo TTreeNode, TActionList,  ...
Bom aí vem a pergunta. Como o Delphi sabe qual é a lista de elementos da minha coleção quando se trata de um objeto?
Se você for na definição da classe TStrings encontrará um método com o nome "GetEnumerator" cujo retorno é um objeto do tipo TStringsEnumerator. E é exatamente esse método que será usado pelo ForEach para acessar os elementos da lista.

Para ser mais específico, um pré-requisito para usar o ForEach com objetos (instâncias de classes) é justamente a existência e implementação do método GetEnumerator pela classe do objeto.
Se você tentar usar um objeto qualquer o próprio compilador irá notificar sobre esse requisito, apresentando uma mensagem semelhante:

[DCC Error] Unit1.pas(145): E2431 for-in statement cannot operate on collection type 'TMyList' because 'TMyList' does not contain a member for 'GetEnumerator', or it is inaccessible

Além disso, o método GetEnumerator não pode retornar qualquer valor, ele deverá retornar uma instância cuja classe possua dois métodos específicos (GetCurrent e MoveNext) que serão utilizados para instruir o ForEach a percorrer os itens do objeto.
O método GetCurrent será utilizado para retornar o valor do atual elemento da lista e o método MoveNext irá posicionar o cursor no próximo item ou retornando False quando não há mais elementos na lista.

Vamos criar um exemplo simples para ilustrar como isso é feito.

TMyList = class;

TMyListEnumerator = class
private
  FIndex: Integer;
  FMyItems: TMyList;
public
  constructor Create(ARef: TMyList);
  function GetCurrent: Integer; inline;
  function MoveNext: Boolean;
  property Current: Integer read GetCurrent;
end;

A classe TMyListEnumerator será a responsável por acessar a lista de valores de TMyList.
O layout acima é exatamente o que você precisa implementar para todas as classes a qual deseja dar suporte ao ForEach, modificando apenas o tipo de retorno de acordo com o tipo de item das classes. Por exemplo, se coleção mantém uma lista de valores de ponto flutuante, substitua a o tipo "Integer" (no layout acima) por "float".

TMyList = class
  protected
    FName: string;
    FItems: array of Integer;

  public
    destructor Destroy; override;
    function Add(const Value: Integer): Integer;
    function GetEnumerator: TMyListEnumerator;
end;

A classe TMyList é nossa coleção a qual mantém um array de valores inteiros (propriedade FItems).
Agora observe a implementação dos métodos da coleção. Nada de diferente a não ser pelo método GetEnumerator.

destructor TMyList.Destroy;
begin
  SetLength(FItems, 0);
  inherited;
end;

function TMyList.Add(const Value: Integer): Integer;
begin
  Result := Length(FItems);
  SetLength(FItems, Result + 1);

  FItems[Result] := Value;
end;

function TMyList.GetEnumerator: TMyListEnumerator;
begin
  Result := TMyListEnumerator.Create(Self);
end;

A implementação da nosso classe de enumeração não tem nada de complexo ou diferente, e o próprio código é auto explicativo.

constructor TMyListEnumerator.Create(ARef: TMyList);
begin
  inherited Create;
  FIndex := -1;
  FMyItems := ARef;
end;

function TMyListEnumerator.GetCurrent: Integer;
begin
  Result := FMyItems.FItems[FIndex];
end;

function TMyListEnumerator.MoveNext: Boolean;
begin
  Result := FIndex < (Length(FMyItems.FItems) - 1);
  if Result then
    Inc(FIndex);
end;

Caso tenha tido algum tipo de dificuldade, faça o download do projeto de exemplo (link http://rapidshare.com/files/406448596/ForEach.7z) que contém todo o código fonte utilizado nesse artigo.

Nenhum comentário: