Fenner
A linguagem

Nulos e exceções

O valor que pode não existir (?T, ??, ?., !), try/except/finally, raise e o que acontece com erros de hardware.

O valor que pode não existir#

nullable.fxp
program Nullable;

type
  TCustomer = class(TObject)
  public
    Age: Int32;
  end class;

var
  Age: ?Int32;            // o valor que pode não existir
  Customer: TCustomer;
  N: Int32;
begin
  WriteLn('tem valor? ', ?Age);
  WriteLn('com padrão: ', Age ?? -1);      // vazio vira -1

  Age := 42;
  WriteLn('tem valor? ', ?Age);
  N := Age!;                               // exige valor
  WriteLn('N = ', N);

  Age := Null;
  Customer := nil;
  N := 7;
  N := Customer?.Age;                      // Customer é nil: esta linha não acontece
  WriteLn('N continua ', N);

  Customer := TCustomer.Create;
  Customer.Age := 30;
  N := Customer?.Age;
  WriteLn('N agora ', N);

  // if como valor, nas duas grafias
  WriteLn(if ?Customer then Customer.Age else 0);
  WriteLn(?Customer ? Customer.Age : 0);
end.
Saída
tem valor? FALSE
com padrão: -1
tem valor? TRUE
N = 42
N continua 7
N agora 30
30
30

?T é o NullableT da RTL escrito curto. A RTL o declara a partir de um genérico (TNullable<T>: um IsValid e um Data); o compilador não fabrica o tipo, ele o procura, e reconhece qualquer record com essa forma, venha de onde vier.

FormaO que faz
X: ?Int32declara o anulável; Null é o vazio, e 42 o atribui
?Xpergunta se há valor. Num anulável é o IsValid; num ponteiro, objeto, interface ou tipo procedural é o <> nil. Num tipo que sempre tem valor é erro de compilação (Int32 sempre tem valor)
A ?? Bo valor de A, ou B se A estiver vazio. Só o lado necessário é avaliado
E!exige o valor; se não houver, a linha não acontece
X?.Macesso guardado: se X for nil, a linha não acontece

Atribuir um anulável a um Int32 direto (F := I;) compila, mas é uma conversão que falha: se I estiver vazio, o programa levanta CastError em execução. Para dizer o que fazer com o vazio, escolha: F := I ?? 0 (usa um padrão) ou F := I! (a linha não acontece).

"A linha não acontece"#

E! e X?.M dizem a mesma coisa, e isso não pode ser respondido dentro da expressão (um Int32 não tem como valer "nada"). Então a pergunta sobe e o comando inteiro fica sob um if:

Fenner
F := X?.Idade;
// vira:  $nz1 := X;  if $nz1 <> nil then F := $nz1.Idade;

O objeto oculto existe para o objeto não ser lido duas vezes, e é fraco (um apelido de um instante, não posse). Várias guardas na mesma linha entram todas, ligadas por and. Dentro de um laço, cada comando do corpo tem a sua guarda; a do cabeçalho envolve o laço inteiro.

Compare com o acesso comum: F := X.Idade com X igual a nil estoura, como sempre estourou.

O if que dá um valor#

Fenner
F := if ?X then X.Idade else 0;
F := ?X ? X.Idade : 0;         // o mesmo, na outra grafia

Só o ramo escolhido é avaliado. Tipo gerenciado nos ramos ainda não: só um dos lados roda, e a contagem de referências precisaria de um caminho próprio.

Exceções#

exceptions.fxp
program Exceptions;

type
  EValidation = class(Exception)
  end class;

procedure Check(const AValue: Int32);
begin
  if AValue < 0 then
    raise EValidation.Create('valor negativo: ' + IntToUStr(AValue));
end procedure;

var
  Zero: Int32;
begin
  try
    Check(5);
    Check(-1);
  except
    on E: EValidation do
      WriteLn('validação: ', E.Message);
    on E: Exception do
      WriteLn('outra: ', E.Message);
  end try;

  // atalho: raise com texto levanta uma Exception com essa mensagem
  try
    raise 'só o texto';
  except
    on E: Exception do
      WriteLn(E.Message);
  end try;

  // erros de hardware viram exceções que o try..except pega
  Zero := 0;
  try
    WriteLn(10 div Zero);
  except
    on E: EDivByZero do
      WriteLn('divisão por zero capturada');
  end try;

  // finally roda de qualquer jeito
  try
    try
      Check(-7);
    finally
      WriteLn('finally executou');
    end try;
  except
    on E: Exception do
      WriteLn('chegou ao except: ', E.Message);
  end try;
end.
Saída
validação: valor negativo: -1
só o texto
divisão por zero capturada
finally executou
chegou ao except: valor negativo: -7
Fenner
try
  Check(5);
except
  on E: EValidation do
    WriteLn('validação: ', E.Message);
  on E: Exception do
    WriteLn('outra: ', E.Message);
end try;
  • try … except … end com um tratador on E: Classe do por tipo. O primeiro que casa responde.
  • try … finally … end roda o bloco finally de qualquer jeito, inclusive com Exit, Break ou uma exceção.
  • raise Classe.Create('mensagem') levanta; raise; dentro de um tratador repropaga.
  • raise 'texto' é o atalho: levanta uma Exception com essa mensagem.
  • A mensagem de uma exceção é UString.

Erros de hardware#

A RTL instala um tratador para falhas de hardware, e algumas viram exceções que o try … except pega, como EDivByZero (divisão inteira por zero). A divisão é checada explicitamente antes do sdiv/udiv, com {$config divcheck on|off} (padrão ligado), porque sem isso o otimizador devolvia 0 em silêncio a partir do -O1.

Verificações de faixa e de transbordo#

Duas verificações em execução existem e vêm desligadas por padrão, porque custam:

Fenner
{$config rangechecks on}
{$config overflowchecks on}

A grafia clássica {$RANGECHECKS ON} e {$OVERFLOWCHECKS ON} também vale.