Case of String

Como você sabe, a instrução case é uma alternativa mais legível para uma série de condições if aninhadas. Diferente de outras linguagens populares, a linguagem Delphi não permite usar case diretamente com strings, uma necessidade bem comum.

Pode-se até tentar explicar (mas não justificar) a limitação considerando as otimizações que o compilador realiza no código de um case. Limitando case aos tipos ordinais (numerais, enumeradores, Char e Boolean), o compilador Delphi ordena os valores de opções do para gerar um código mais eficiente.

O código abaixo exemplifica o uso regular do case:

procedure ExemploCase(i: Integer);
begin
  case i of
    1: { faz uma coisa };
    2: { faz outra coisa };
    3..5: { faz ainda outra };
  else
    { senão faz isso mesmo }
  end;
end;

Vamos às alternativas para implementar o equivalente a um case com strings. A primeira idéia é fazer mesmo uma seqüência de ifs aninhados.

procedure ExemploStr1(S: String);
begin
  if S = 'banana' then
    {...}
  else
    if S = 'pera' then
      {...}
    else
      if (S = 'abacaxi') or (S = 'limao') then
        {...}
      else
        {...}
end;

É ruim de ler, difícil de manter, aumenta a complexidade (ciclomática) do código e é propenso a erros de sintaxe. Ainda por cima, se o programador não se cuidar com os begins e ends, pode até produzir alguns bugs difíceis de encontrar.

Uma solução é declarar um array de strings constante, fazer uma busca sequencial simples para encontrar a posição da string e depois usar o índice da string em um case:

procedure ExemploStr2(S: String);
const
  NumFrutas = 4;
  Frutas: array[1..NumFrutas] of String =
    ('banana', 'pera', 'abacaxi', 'limao');
var
  Index: Integer;
begin
  for Index := 1 to NumFrutas do
    if S = Frutas[Index] then
      break;
  case Index of
    1: {...};
    2: {...};
    3..4: {...}
  else
    {...}
  end;
end;

Um problema comum às duas alternativas acima é a comparação de strings, que diferença de maiúsculas e minúsculas. Para resolver isso, basta substitutir

if (S = Frutas[Index]) then ...

pela função

if SameText(S, Frutas[Index]) then ...

Uma terceira alternativa, mais prática e legível, é usar a função StrIndex abaixo para obter o índice de um valor string em um array dinâmico de strings. StrIndex foi adaptada da unit JclStrings da JCL.

function StrIndex(const S: string; const List: array of string): Integer;
var
  I: Integer;
begin
  Result := -1;
  for I := Low(List) to High(List) do
  begin
    if AnsiSameText(S, List[I]) then
    begin
      Result := I + 1;
      Break;
    end;
  end;
end;

Usando essa função fica muito fácil implementar um case de strings eficiente e legível. Por exemplo, a mesma procedure utilizando StrIndex ficaria:

procedure ExemploStr3(S: String);
begin
  case StrIndex(S, ['banana', 'pera', 'abacaxi', 'limao']) of
    1: {...};
    2: {...};
    3..4: {...}
  else
    {...}
  end;
end;

A rotina StrIndex acima pode ser usada independente da JCL e tem a diferença de retornar um índice baseado em 1, não em 0, como a função da JCL. No meu caso, eu prefiro usar a própria função da JclStrings, já que uso com freqüência também outras funções da JCL.

Class helpers

Uma situação comum para quem usa a arquitetura DataSnap é tratar manualmente valores de campos no evento OnBeforeUpdate do componente TDataSetProvider. O evento tem um parâmetro DeltaDS que é um dataset. Para pegar o valor atual de um campo você usa a propriedade NewValue, certo? Bem, na verdade isso só é valido se o campo tiver sido editado pelo usuário (ou se for um Insert). Se, por outro lado, for um Update ou um Delete e o valor anterior do campo tiver sido mantido, você deve pegar o valor de OldValue. Não é muito intuitivo, certo?

Seria bom se existisse uma propriedade CurrentValue que sempre retornasse o valor atual do TField, seja qual for o estado do dataset. Read more

DBHelpers

DBHelpers é a minha unit de class helpers para TField e TDataSet. Você pode fazer o download do código fonte de dbhelpers.zip.

Class helper é um novo recurso da linguagem Delphi (salvo engano, disponível a partir do BDS2006) para estender a funcionalidade de uma classe existente com novos métodos ou propriedades.

Diferente do uso de herança, ao invés de criar uma classe descendente você declara um tipo “class helper for (AClass)”, onde AClass é a classe que você quer estender. O compilador entende que os seus métodos são válidos no escopo da classe associada (AClass) e de seus descendentes. Por exemplo, os métodos de TDataSetHelper podem ser usados como se pertencessem às classes TClientDataSet, TSQLDataSet ou qualquer outro desdente de TDataSet.

Um class helper não tem dados, mas pode acessar todos os campos, propriedades e métodos protegidos ou públicos da classe associada. É a versão “limpinha” do hack de fazer um typecast forçado de uma classe para outra classe descendente local. Em outras palavras, escrever um class helper é como legalizar na Prefeitura o “puxadinho” que você queria construir sobre a laje do barraco.

Essa é a interface da versão inicial de DBHelpers, que tem vários métodos úteis para uso em aplicações clientes (TDataSetHelper e TFieldHelper.AsStringTrim) e servidores DataSnap (CurrentValue, IsEmpty e OldIsNull de TFieldHelper) :

22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
type
  TForEachRow = procedure (Sender: TDataSet; var Abort: Boolean) of object;
 
  TDataSetHelper = class helper for TDataSet
  public
    procedure DeleteAll;
    procedure ForEach(AMethod: TForEachRow; const AFilter: String = '');
    procedure GetFieldValues(Strings: TStrings; Field: TField;
      Distinct: Boolean = True); overload;
    procedure GetFieldValues(Strings: TStrings; const FieldName: String;
      Distinct: Boolean = True); overload;
    procedure GetParamValues(Params: TParams);
  end;
 
  TFieldHelper = class helper for TField
  private
    function GetAsStringTrim: String;
    procedure SetAsStringTrim(const Value: String);
    function GetCurrentValue: Variant;
    procedure SetCurrentValue(const Value: Variant);
  public
    property AsStringTrim: String read GetAsStringTrim write SetAsStringTrim;
    property CurrentValue: Variant read GetCurrentValue write SetCurrentValue;
    function IsEmpty: Boolean;
    function OldIsNull: Boolean;
  end;

Não tenho por enquanto mais documentação além do próprio código fonte, que é bem simples, mas sintam-se a vontade para perguntar se tiverem dúvidas.