Fenner
A linguagem

"Acesso condicional ?. e ??="

A?.B?.C sem erro quando um elo é nil, ?? para o valor de reserva e ??= para atribuir só se vazio.

O que é e por que existe#

Uma cadeia como LHead.Next.Next.Value falha com violação de acesso se qualquer elo for nil. Escrever um if por elo polui o código. O acesso condicional do C# (?.) interrompe a cadeia ao primeiro elo vazio e devolve "vazio" em vez de falhar. A página Nulos e exceções apresenta o tipo anulável ?T; aqui está o resto.

padrão (off)NullConditional on
F := X?.Idade; com X = nila linha não aconteceX?.Idade é "vazio"; converter para Int32 falha em execução
X?.M(a); (comando)erronada acontece se X = nil (nem a é avaliado)
X?[i]dois tokensindexação condicional
A ?? Bigualigual

Semântica#

  • A?.B, A?.M(args), A?[i]: se A é nulo, o resto da cadeia não corre, inclusive os argumentos. A?.B.C não avalia .C com A nulo; A?.B?.C testa os dois elos. A é avaliado uma vez.
  • Em comando, A?.M; não faz nada se um elo é nulo; A?.Free; zera A.
  • A?.B := X é erro, como no C# (se A for nulo não há onde gravar).
  • Operandos aceitos: classe, interface, ponteiro, reference to, vetor dinâmico e anulável. Cadeia, inteiro e record sem Nullable dão FE2080.
  • Tipo do resultado: referências ficam do mesmo tipo (vazio = nil); qualquer outro tipo (Int32, Bool8…) vira o Nullable dele (NullableInt32…), procurado pelo nome.

?? e ??=#

  • A ?? B é o valor ou a reserva; só avalia B se preciso. A?.B ?? 0 é Int32.
  • X ??= D atribui D só se X é nulo, e D só é avaliado nesse caso: LHead.Next.Next ??= TNode.Create(3, nil). Vale para Nullable, classe, interface, ponteiro e vetor dinâmico; X não pode conter chamadas (FE2089). Sem a chave, ??= não existe.
rc-nullcond.fxp
program Nullable;
// Demo 31: valores que podem não existir - Nullable (?T), "?.", "??" e "??=".
//
// ?Int32 é um Int32 que também pode estar "vazio" (Null).
// "A?.B" lê B só se A não for nil; senão o resultado é "vazio" e o resto
// da cadeia nem executa. "??" dá um valor de reserva. "??=" atribui só se vazio.

{$config NullConditional on}     // liga o "?." no estilo C# (desligado por padrão)

type
  TNode = class
  public
    Value: Int32;
    Next: TNode;
    constructor Create(AValue: Int32; ANext: TNode);
  end class;

constructor TNode.Create(AValue: Int32; ANext: TNode);
begin
  Value := AValue;
  Next := ANext;
end constructor;

var
  LAge: ?Int32;                  // anulável: começa vazio
  LHead, LNone: TNode;
  LValue: Int32;
begin
  // --- Nullable ---
  WriteLn('tem valor? ', ?LAge);
  WriteLn('com reserva: ', LAge ?? -1);     // "??": vazio vira -1
  LAge := 42;
  WriteLn('tem valor? ', ?LAge, ' valor ', LAge!);   // "!" extrai o valor

  // --- ?. em cadeias de objetos ---
  LHead := TNode.Create(1, TNode.Create(2, nil));   // 1 -> 2 -> fim
  LNone := nil;

  // sem "?." LNone.Value daria erro de acesso; com ele, vira vazio
  LValue := LNone?.Value ?? 0;
  WriteLn('de nil: ', LValue);
  LValue := LHead?.Value ?? 0;
  WriteLn('da cabeça: ', LValue);
  LValue := LHead?.Next?.Value ?? 0;
  WriteLn('segundo nó: ', LValue);
  // o terceiro não existe: o "?." interrompe a cadeia sem erro
  LValue := LHead?.Next?.Next?.Value ?? -1;
  WriteLn('terceiro nó: ', LValue);

  // --- ??= : atribui só se estiver vazio ---
  LHead.Next.Next ??= TNode.Create(3, nil);   // estava nil: cria
  LHead.Next.Next ??= TNode.Create(99, nil);  // já existe: nem avalia o lado direito
  WriteLn('terceiro agora: ', LHead.Next.Next.Value);
end.
Saída
tem valor? FALSE
com reserva: -1
tem valor? TRUE valor 42
de nil: 0
da cabeça: 1
segundo nó: 2
terceiro nó: -1
terceiro agora: 3

Demo 31. Nullable, ??, cadeia que se interrompe sem erro e ??= que cria o nó só quando falta.

Comparações e contas "lifted"#

Com a chave ligada, um anulável vindo de ?. compara com o valor de dentro: A?.B > 5 é False se vazio; A?.B <> 5 é True; A?.B = nil pergunta só se há valor. Aritmética (+ - * / div mod and or xor shl shr, - e not unários) com operando vazio dá o Nullable vazio, e os dois operandos são avaliados, como no C#.

Armadilhas#

  • CastError em execução: F := A?.FValor; com F: Int32 compila, mas levanta exceção se vazio. O compilador avisa (FW0227); escreva A?.FValor ?? 0 (ou A?.FValor!).
  • C ? [1, 2] : [3] com a chave ligada exige o espaço depois do ?: colado, C?[ é o índice condicional.
  • ?. seguido de dígito deixa de ser o token (C?.5 : 1).
  • Fora do escopo: A?.B sobre procedure … of object como operando, e ??= sobre property.

Veja também#