Fenner
A linguagem

case com guardas e padrões

when, ligação (var N), is T(var X) e o case-expressão exaustivo.

O que é e por que existe#

O case clássico só compara rótulos. Com as extensões abaixo ele deixa de precisar de uma cadeia de if quando a escolha depende de uma condição extra, do tipo do objeto ou do valor casado. Sem nenhuma extensão, o case é o de sempre (e continua virando switch no código gerado).

Fenner
case X of
  1..5 when APositive:       ...   // guarda: se for falsa, segue para o PRÓXIMO braço
  7, 9 (var N) when N > 8:   ...   // (var N) liga o valor casado a N
else ... end;

case AShape of                     // seletor objeto ou interface
  is TSquare(var Q) when Q.Side > 10: ...   // Q já vem tipado como TSquare
  is TCircle(var C): ...
end;

Result := case AColor of           // case como expressão
  cRed when AStrong: 3;
  cRed: 1;
  cGreen: 2;
  cBlue: 4;                        // todos os membros cobertos: não precisa de else
end case;

Regras#

  • O seletor é avaliado uma vez; os braços são tentados na ordem escrita e a primeira guarda que passa ganha.
  • when é palavra contextual: só vale depois dos rótulos do braço.
  • (var N) vem antes do when, só em seletor ordinal, e não vale em braço com vários rótulos (de qual seria o valor?).
  • is T(var F): F é uma referência fraca (emprestada, como o E de on E: TFoo do). Quem segura o objeto é o temporário do case; não guarde F além do braço.
  • Case-expressão: aceita when por braço. Com seletor de enum e braços sem guarda que cobrem todos os membros, o else é dispensado. Se o último braço tem guarda, o else volta a ser obrigatório.
  • Também aceita padrões de record e de tupla: TPonto(X: 0, Y: var Py), (5, var S); veja Tipos soma: choice para os padrões aninhados.
rc-match.fxp
program Match;
// Demo: `case` com guardas (`when`), ligação (`(var N)`), padrão de tipo (`is T(var X)`)
// e `case` como expressão (exaustivo, sem `else` quando cobre todo o enum).

type
  TColor = (cRed, cGreen, cBlue);

  TShape = class(TObject)
  end class;
  TSquare = class(TShape)
  public
    Side: Int32;
  end class;
  TCircle = class(TShape)
  public
    Radius: Int32;
  end class;

// A guarda `when` é testada depois de o rótulo casar. Se for falsa, o case segue para o PRÓXIMO braço.
function Classify(X: Int32; APositive: Bool8): UString;
begin
  case X of
    1..5 when APositive: Result := 'faixa com flag';
    1..5: Result := 'faixa simples';                     // pega o que a guarda recusou
    7, 9 (var N) when N > 8: Result := 'grande ' + IntToUStr(N);   // (var N) liga o valor casado
    7: Result := 'sete';
  else
    Result := 'outro';
  end case;
end function;

// `is TSquare(var Q)` testa o tipo E já entrega a variável convertida.
function Describe(AShape: TShape): UString;
begin
  case AShape of
    is TSquare(var Q) when Q.Side > 10: Result := 'quadrado grande';
    is TSquare(var Q): Result := 'quadrado de lado ' + IntToUStr(Q.Side);
    is TCircle(var C): Result := 'círculo de raio ' + IntToUStr(C.Radius);
  else
    Result := 'forma genérica';
  end case;
end function;

// Como expressão, o case devolve valor; sem `else` o compilador exige cobrir todo o enum.
function Weight(AColor: TColor; AStrong: Bool8): Int32;
begin
  Result := case AColor of
    cRed when AStrong: 3;
    cRed: 1;
    cGreen: 2;
    cBlue: 4;
  end case;
end function;

var
  LSquare: TSquare;
  LCircle: TCircle;
begin
  WriteLn(Classify(3, True), '; ', Classify(3, False), '; ', Classify(9, True),
          '; ', Classify(7, True), '; ', Classify(50, True));
  LSquare := TSquare.Create;
  LSquare.Side := 12;
  WriteLn(Describe(LSquare));
  LSquare.Side := 4;
  WriteLn(Describe(LSquare));
  LCircle := TCircle.Create;
  LCircle.Radius := 5;
  WriteLn(Describe(LCircle), '; ', Describe(TShape.Create));
  WriteLn(Weight(cRed, True), ' ', Weight(cRed, False), ' ', Weight(cGreen, False), ' ', Weight(cBlue, False));
end.
Saída
faixa com flag; faixa simples; grande 9; sete; outro
quadrado grande
quadrado de lado 4
círculo de raio 5; forma genérica
3 1 2 4

Demo 42. Guarda, ligação, is T(var X) com guarda e o case-expressão sobre enum.

Armadilhas e erros típicos#

  • Case-expressão que não cobre o enum é erro, não aviso: FE0804 ("o case que dá um VALOR sem else precisa cobrir todos os membros do enum, e falta cBlue"). Um case que dá valor não pode "seguir" sem resultado.
  • Depois dessa mensagem boa pode vir um erro em cascata (FE0218/FE1254: o "end case" fecha um case, mas o que está aberto é uma function). Corrija o primeiro e o resto some.
  • Com guarda ou ligação, o case deixa de ser um switch da LLVM e perde a checagem de rótulo duplicado.
  • A exaustividade rastreia até 256 rótulos por case-expressão; acima disso exige else.
  • No case-expressão, o seletor não pode ter efeito colateral.
  • O aviso opt-in CaseNotExhaustive (Avisos opt-in) acusa o case de comando sobre enum sem else que esquece membros.

Veja também#