Condividi tramite


Assegnazione null-condizionale

Annotazioni

Questo articolo è una specifica delle funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.

Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono riportate nelle note pertinenti della riunione di progettazione linguistica (LDM) .

Ulteriori dettagli sul processo di adozione delle specifiche di funzionalità nello standard del linguaggio C# sono disponibili nell'articolo sulle specifiche .

Questione prioritaria: https://github.com/dotnet/csharplang/issues/8677

Riassunto

Consente l'assegnazione in modo condizionale all'interno di un'espressione a?.b o a?[b] .

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();
}

Motivazione

Un'ampia gamma di casi d'uso motivanti si possono trovare nel problema sostenuto. Le motivazioni principali includono:

  1. Parità tra proprietà e Set() metodi.
  2. Collegamento di gestori eventi nel codice dell'interfaccia utente.

Progettazione dettagliata

  • La parte destra dell'assegnazione viene valutata solo quando il ricevitore dell'accesso condizionale è diverso da null.
// 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();
  • Sono consentite tutte le forme di assegnazione composta.
a?.b -= M(); // ok
a?.b += M(); // ok
// etc.
  • Se viene usato il risultato dell'espressione, il tipo dell'espressione deve essere noto come di un tipo valore o di un tipo riferimento. Questo comportamento è coerente con i comportamenti esistenti sugli accessi condizionali.
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
}
  • Le espressioni di accesso condizionale non sono ancora gli lvalue e non è ancora consentito, ad esempio, applicare un'operazione ref su di esse.
M(ref a?.b); // error
  • Non è consentito assegnare riferimenti a un accesso condizionale. Il motivo principale è che l'unico modo per accedere in modo condizionale a una variabile ref è un campo ref e gli struct ref non possono essere usati nei tipi valore nullable. Se in futuro si presentasse uno scenario valido per un'assegnazione di riferimento condizionale, potremmo aggiungere il supporto in quel 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'.
}
  • Non è possibile, ad esempio, effettuare assegnazioni sugli accessi condizionali tramite la decostruzione. Si prevede che sarà raro che qualcuno voglia farlo e non sarà un notevole svantaggio doverlo fare su più espressioni di assegnazione separate.
(a?.b, c?.d) = (x, y); // error
a?.b++; // error
--a?.b; // error
  • Questa funzionalità in genere non funziona quando il ricevitore dell'accesso condizionale è un tipo di valore. Ciò è dovuto al fatto che rientra in uno dei due casi seguenti:
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 propone di rilassarsi, nel qual caso potremmo definire un comportamento ragionevole per a?.b = c, quando a è un System.Nullable<T> e b è una proprietà con un setter di sola lettura.

Specificazione

La grammatica dell'assegnazione condizionale Null è definita come segue:

null_conditional_assignment
    : null_conditional_member_access assignment_operator expression
    : null_conditional_element_access assignment_operator expression

Per informazioni di riferimento, vedere §11.7.7 e §11.7.11 .

Quando l'assegnazione condizionale Null appare in un'istruzione-espressione, la sua semantica è la seguente:

  • P?.A = B è equivalente a if (P is not null) P.A = B;, ad eccezione del fatto che P viene valutato una sola volta.
  • P?[A] = B è equivalente a if (P is not null) P[A] = B, ad eccezione del fatto che P viene valutato una sola volta.

In caso contrario, la semantica è la seguente:

  • P?.A = B equivale a (P is null) ? (T?)null : (P.A = B), dove T è il tipo di risultato di , ad eccezione del P.A = Bfatto che P viene valutato una sola volta.
  • P?[A] = B equivale a (P is null) ? (T?)null : (P[A] = B), dove T è il tipo di risultato di , ad eccezione del P[A] = Bfatto che P viene valutato una sola volta.

Implementazione

La grammatica nello standard attualmente non corrisponde fortemente alla progettazione della sintassi usata nell'implementazione. Ci aspettiamo che rimanga il caso dopo l'implementazione di questa funzionalità. La progettazione della sintassi nell'implementazione non dovrebbe effettivamente cambiare, ma solo il modo in cui viene usato cambierà. Per esempio:

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

Esempi complessi

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;

Svantaggi

La scelta di mantenere l'assegnazione all'interno dell'accesso condizionale introduce alcune operazioni aggiuntive per l'IDE, che include molti percorsi di codice che devono funzionare all'indietro da un'assegnazione per identificare l'elemento assegnato.

Le alternative

È invece possibile rendere sintatticamente un ?. figlio dell'oggetto =. In questo modo, qualsiasi gestione delle = espressioni deve essere consapevole dell'condizionalità del lato destro in presenza di ?. a sinistra. Fa sì che la struttura della sintassi non corrisponda altrettanto fortemente alla semantica.

Domande non risolte

Incontri di design