Partilhar via


Atribuição condicional nula

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da Language Design Meeting (LDM).

Você pode saber mais sobre o processo de adoção de especificações de recursos no padrão de linguagem C# no artigo sobre as especificações .

Edição campeã: https://github.com/dotnet/csharplang/issues/8677

Resumo

Permite que a atribuição ocorra condicionalmente dentro de uma a?.b ou a?[b] expressão.

using System;

class C
{
    public object obj;
}

void M(C? c)
{
    c?.obj = new object();
}
using System;

class C
{
    public event Action E;
}

void M(C? c)
{
    c?.E += () => { Console.WriteLine("handled event E"); };
}
void M(object[]? arr)
{
    arr?[42] = new object();
}

Motivação

Uma variedade de casos de uso motivadores pode ser encontrada na questão defendida. As principais motivações incluem:

  1. Paridade entre propriedades e Set() métodos.
  2. Anexando manipuladores de eventos no código da interface do usuário.

Design Detalhado

  • O lado direito da atribuição só é avaliado quando o recetor do acesso condicional não é nulo.
// M() is only executed if 'a' is non-null.
// note: the value of 'a.b' doesn't affect whether things are evaluated here.
a?.b = M();
  • Todas as formas de atribuição composta são permitidas.
a?.b -= M(); // ok
a?.b += M(); // ok
// etc.
  • Se o resultado da expressão for usado, o tipo da expressão deve ser conhecido como sendo de um tipo de valor ou um tipo de referência. Isso é consistente com os comportamentos existentes em acessos condicionais.
class C<T>
{
    public T? field;
}

void M1<T>(C<T>? c, T t)
{
    (c?.field = t).ToString(); // error: 'T' cannot be made nullable.
    c?.field = t; // ok
}
  • As expressões de acesso condicional ainda não são lvalues e ainda não é permitido, por exemplo, levá-las ref a.
M(ref a?.b); // error
  • Não é permitido recusar atribuir a um acesso condicional. A principal razão para isso é que a única maneira de acessar condicionalmente uma variável ref é um campo ref, e ref structs são proibidos de serem usados em tipos de valor anuláveis. Se um cenário válido para uma atribuição ref condicional surgisse no futuro, poderíamos adicionar suporte naquele momento.
ref struct RS
{
    public ref int b;
}

void M(RS a, ref int x)
{
  a?.b = ref x; // error: Operator '?' can't be applied to operand of type 'RS'.
}
  • Não é possível, por exemplo, atribuir acessos condicionais através da atribuição de desconstrução. Prevemos que será raro as pessoas quererem fazer isso, e não será uma desvantagem significativa precisar fazê-lo em várias expressões de atribuição separadas.
(a?.b, c?.d) = (x, y); // error
  • Não há suporte para operadores de incremento/decréscimo.
a?.b++; // error
--a?.b; // error
  • Esse recurso geralmente não funciona quando o recetor do acesso condicional é um tipo de valor. Isso porque ele se enquadrará em um dos dois casos a seguir:
void Case1(MyStruct a)
    => a?.b = c; // a?.b is not allowed when 'a' is of non-nullable value type

void Case2(MyStruct? a)
    => a?.b = c; // `a.Value` is not a variable, so there's no reasonable meaning to define for the assignment

readonly-setter-calls-on-non-variables.md propõe relaxar isso, caso em que poderíamos definir um comportamento razoável para a?.b = c, quando a é um System.Nullable<T> e b é uma propriedade com um setter somente leitura.

Especificação

A gramática de atribuição condicional nula é definida da seguinte forma:

null_conditional_assignment
    : null_conditional_member_access assignment_operator expression
    : null_conditional_element_access assignment_operator expression

Ver referência nos pontos 11.7.7 e 11.7.11 .

Quando a atribuição condicional nula aparece em uma instrução de expressão, sua semântica é a seguinte:

  • P?.A = B é equivalente a if (P is not null) P.A = B;, exceto que P é avaliado apenas uma vez.
  • P?[A] = B é equivalente a if (P is not null) P[A] = B, exceto que P é avaliado apenas uma vez.

Caso contrário, sua semântica é a seguinte:

  • P?.A = B é equivalente a (P is null) ? (T?)null : (P.A = B), onde T é o tipo de resultado de P.A = B, exceto que P é avaliado apenas uma vez.
  • P?[A] = B é equivalente a (P is null) ? (T?)null : (P[A] = B), onde T é o tipo de resultado de P[A] = B, exceto que P é avaliado apenas uma vez.

Execução

A gramática no padrão atualmente não corresponde fortemente ao design de sintaxe usado na implementação. Esperamos que continue a ser assim depois de esta funcionalidade ser implementada. Não se espera que o design da sintaxe na implementação realmente mude, apenas a maneira como ela é usada mudará. Por exemplo:

graph TD;
subgraph ConditionalAccessExpression
  whole[a?.b = c]
end
subgraph  
  subgraph WhenNotNull
    whole-->whenNotNull[".b = c"];
    whenNotNull-->.b;
    whenNotNull-->eq[=];
    whenNotNull-->c;
  end
  subgraph OperatorToken
    whole-->?;
  end
  subgraph Expression
    whole-->a;
  end
end

Exemplos complexos

class C
{
    ref int M() => /*...*/;
}

void M1(C? c)
{
    c?.M() = 42; // equivalent to:
    if (c is not null)
        c.M() = 42;
}

int? M2(C? c)
{
    return c?.M() = 42; // equivalent to:
    return c is null ? (int?)null : c.M() = 42;
}
M(a?.b?.c = d); // equivalent to:
M(a is null
    ? null
    : (a.b is null
        ? null
        : (a.b.c = d)));
return a?.b = c?.d = e?.f; // equivalent to:
return a?.b = (c?.d = e?.f); // equivalent to:
return a is null
    ? null
    : (a.b = c is null
        ? null
        : (c.d = e is null
            ? null
            : e.f));
}
a?.b ??= c; // equivalent to:
if (a is not null)
{
    if (a.b is null)
    {
        a.b = c;
    }
}

return a?.b ??= c; // equivalent to:
return a is null
    ? null
    : a.b is null
        ? a.b = c
        : a.b;

Desvantagens

A opção de manter a atribuição dentro do acesso condicional introduz algum trabalho adicional para o IDE, que tem muitos caminhos de código que precisam trabalhar para trás de uma atribuição para identificar a coisa que está sendo atribuída.

Alternativas

Poderíamos, em vez disso, fazer do ?. sintaticamente um filho do =. Isso faz com que qualquer manipulação de = expressões precise tomar consciência da condicionalidade do lado direito na presença do ?. lado esquerdo. Isso também faz com que a estrutura da sintaxe não corresponda tão fortemente à semântica.

Questões por resolver

Reuniões de design