Modifiche ai criteri di ricerca per C# 9.0
Nota
Questo articolo è una specifica di 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 acquisite nelle note
Puoi ottenere maggiori informazioni sul processo di adozione dei feature speclet nello standard del linguaggio C# nell'articolo sulle specifiche di .
Stiamo prendendo in considerazione una piccola manciata di miglioramenti alla corrispondenza dei criteri per C# 9.0 che hanno una sinergia naturale e funzionano bene per risolvere diversi problemi di programmazione comuni:
- modelli di tipo https://github.com/dotnet/csharplang/issues/2925
- https://github.com/dotnet/csharplang/issues/1350 modelli racchiusi tra parentesi per applicare o evidenziare la precedenza dei nuovi combinatori
-
https://github.com/dotnet/csharplang/issues/1350 modelli di
and
congiuntivi che richiedono la corrispondenza tra due modelli diversi; -
https://github.com/dotnet/csharplang/issues/1350 modelli di
or
disgiuntivo che richiedono la corrispondenza di uno dei due modelli diversi; -
https://github.com/dotnet/csharplang/issues/1350 modelli di
not
negati che richiedono un criterio specifico non per la corrispondenza; e - https://github.com/dotnet/csharplang/issues/812 Modelli relazionali che richiedono che il valore di input sia minore, minore o uguale a una determinata costante, ecc.
Modelli racchiusi tra parentesi
I modelli racchiusi tra parentesi consentono al programmatore di inserire parentesi intorno a qualsiasi modello. Ciò non è così utile con i pattern esistenti in C# 8.0, tuttavia i nuovi combinatori di pattern introducono una precedenza che il programmatore potrebbe voler sovrascrivere.
primary_pattern
: parenthesized_pattern
| // all of the existing forms
;
parenthesized_pattern
: '(' pattern ')'
;
Modelli di tipo
Consentiamo un tipo come modello:
primary_pattern
: type-pattern
| // all of the existing forms
;
type_pattern
: type
;
In questo modo il esistente è di tipo expression essere un is-pattern-expression in cui il criterio è un modello di tipo, anche se non si modifica l'albero della sintassi prodotto dal compilatore.
Un problema di implementazione sottile è che questa grammatica è ambigua. Una stringa, ad esempio a.b
, può essere analizzata come un nome completo (in un contesto di tipo) o un'espressione punteggiata (in un contesto di espressione). Il compilatore è già in grado di trattare un nome qualificato allo stesso modo di un'espressione con il punto per gestire qualcosa come e is Color.Red
. L'analisi semantica del compilatore verrà ulteriormente estesa in modo da poter associare un modello costante (sintattico) (ad esempio un'espressione punteggiata) come tipo per considerarlo come modello di tipo associato per supportare questo costrutto.
Dopo questa modifica, sarà possibile scrivere
void M(object o1, object o2)
{
var t = (o1, o2);
if (t is (int, string)) {} // test if o1 is an int and o2 is a string
switch (o1) {
case int: break; // test if o1 is an int
case System.String: break; // test if o1 is a string
}
}
Modelli relazionali
I modelli relazionali consentono al programmatore di esprimere che un valore di input deve soddisfare un vincolo relazionale rispetto a un valore costante:
public static LifeStage LifeStageAtAge(int age) => age switch
{
< 0 => LifeStage.Prenatal,
< 2 => LifeStage.Infant,
< 4 => LifeStage.Toddler,
< 6 => LifeStage.EarlyChild,
< 12 => LifeStage.MiddleChild,
< 20 => LifeStage.Adolescent,
< 40 => LifeStage.EarlyAdult,
< 65 => LifeStage.MiddleAdult,
_ => LifeStage.LateAdult,
};
I modelli relazionali supportano gli operatori relazionali <
, <=
, >
e >=
in tutti i tipi predefiniti che supportano tali operatori relazionali binari con due operandi dello stesso tipo in un'espressione. In particolare, sono supportati tutti questi modelli relazionali per sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
, decimal
, nint
e nuint
.
primary_pattern
: relational_pattern
;
relational_pattern
: '<' relational_expression
| '<=' relational_expression
| '>' relational_expression
| '>=' relational_expression
;
L'espressione è necessaria per restituire un valore costante. Si tratta di un errore se il valore costante è double.NaN
o float.NaN
. Si tratta di un errore se l'espressione è una costante Null.
Quando l'input è un tipo per il quale è definito un operatore relazionale binario predefinito appropriato, applicabile con l'input come operando sinistro e la costante fornita come operando destro, la valutazione di tale operatore viene considerata come il significato del modello relazionale. In caso contrario, l'input viene convertito nel tipo dell'espressione usando una conversione esplicita nullable o unboxing. Si tratta di un errore in fase di compilazione se non esiste alcuna conversione di questo tipo. Il modello viene considerato non corrispondente se la conversione non riesce. Se la conversione ha esito positivo, il risultato dell'operazione di corrispondenza dei criteri è il risultato della valutazione dell'espressione e OP v
in cui e
è l'input convertito, OP
è l'operatore relazionale e v
è l'espressione costante.
Combinatori di modelli
I combinatori di criteri
Un uso comune di un combinatore sarà il linguaggio
if (e is not null) ...
Più leggibile del linguaggio corrente e is object
, questo modello esprime chiaramente che si sta controllando un valore non Null.
I combinatori di and
e or
saranno utili per testare intervalli di valori
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
In questo esempio viene illustrato che and
avrà una priorità di analisi più elevata, cioè sarà associata più strettamente, rispetto a or
. Il programmatore può usare il modello tra parentesi per rendere esplicita la precedenza:
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
Analogamente a tutti i criteri, questi combinatori possono essere usati in qualsiasi contesto in cui è previsto un criterio, inclusi i criteri annidati, l'espressione di tipo is-pattern, l'espressione di tipo switche il criterio dell'etichetta delle condizioni di un'istruzione switch.
pattern
: disjunctive_pattern
;
disjunctive_pattern
: disjunctive_pattern 'or' conjunctive_pattern
| conjunctive_pattern
;
conjunctive_pattern
: conjunctive_pattern 'and' negated_pattern
| negated_pattern
;
negated_pattern
: 'not' negated_pattern
| primary_pattern
;
primary_pattern
: // all of the patterns forms previously defined
;
Passare a 6.2.5 Ambiguità grammaticali
A causa dell'introduzione del modello di tipo , è possibile che un tipo generico venga visualizzato prima del token =>
. Aggiungiamo quindi =>
al set di token elencati in §6.2.5 Ambiguità grammaticali per consentire la disambiguazione del <
che inizia l'elenco di argomenti di tipo. Vedi anche https://github.com/dotnet/roslyn/issues/47614.
Problemi aperti con le modifiche proposte
Sintassi per gli operatori relazionali
and
, or
e not
una parola chiave contestuale? In tal caso, si verifica una modifica sostanziale (ad esempio, confrontata con il loro uso come designatore in un modello di dichiarazione ).
Semantica (ad esempio tipo) per gli operatori relazionali
Si prevede di supportare tutti i tipi primitivi che possono essere confrontati in un'espressione usando un operatore relazionale. Il significato in casi semplici è chiaro
bool IsValidPercentage(int x) => x is >= 0 and <= 100;
Ma quando l'input non è un tipo primitivo di questo tipo, in quale tipo si tenta di convertirlo?
bool IsValidPercentage(object x) => x is >= 0 and <= 100;
È stato proposto che quando il tipo di input è già una primitiva confrontabile, quello è il tipo di confronto. Tuttavia, quando l'input non è una primitiva comparabile, trattiamo il relazionale come se includesse un test di tipo implicito per il tipo della costante sul lato destro della relazione. Se il programmatore intende supportare più tipi di input, deve essere eseguito in modo esplicito:
bool IsValidPercentage(object x) => x is
>= 0 and <= 100 or // integer tests
>= 0F and <= 100F or // float tests
>= 0D and <= 100D; // double tests
Risultato: il relazionale include un test di tipo implicito per il tipo della costante sul lato destro del relazionale.
Il flusso delle informazioni di tipo da sinistra a destra di and
È stato suggerito che, quando si scrive un combinatore and
, le informazioni sul tipo dedotte a sinistra riguardanti il tipo di primo livello potrebbero trasferirsi verso destra. Per esempio
bool isSmallByte(object o) => o is byte and < 100;
In questo caso, il tipo di input al secondo criterio viene ristretto dal tipo requisiti di sinistra del and
. Definiremo la semantica del restringimento dei tipi per tutti i modelli nel modo seguente. Il tipo ristretto di un P
di criteri è definito come segue:
- Se
P
è un modello di tipo, il tipo ristretto è il tipo del modello di tipo. - Se
P
è un modello di dichiarazione, il tipo ristretto è il tipo corrispondente al modello di dichiarazione. - Se
P
è un modello ricorsivo che fornisce un tipo esplicito, il tipo ristretto è quel tipo. - Se
P
è corrispondente tramite le regole perITuple
, il tipo ristretto è il tipoSystem.Runtime.CompilerServices.ITuple
. - Se
P
è un criterio costante in cui la costante non è la costante Null e in cui l'espressione non ha conversione costante di espressioni nel tipo di input , il tipo ristretto è il tipo della costante. - Se
P
è un modello relazionale in cui l'espressione costante non ha conversione costante di espressioni nel tipo di input , il tipo ristretto è il tipo della costante. - Se
P
è un modello dior
, il tipo ristretto è il tipo comune del tipo ristretto dei sottopattern, qualora esista un tipo comune. A questo scopo, l'algoritmo di tipo comune considera solo le conversioni di identità, boxing e riferimenti impliciti e considera tutti i sottopattern di una sequenza di modelli dior
(ignorando i modelli tra parentesi). - Se
P
è uno schema diand
, il tipo ristretto è il tipo ristretto dello schema giusto. Inoltre, il tipo specifico del motivo a sinistra è il tipo di input del motivo a destra. - In caso contrario, il tipo ristretto di
di è il tipo di input di .
Risultato: la semantica di restringimento precedente è stata implementata.
Definizioni di variabili e assegnazione definita
L'aggiunta di modelli or
e not
crea alcuni nuovi problemi interessanti relativi alle variabili di pattern e all'assegnazione definita. Poiché le variabili possono essere normalmente dichiarate al massimo una volta, sembra che qualsiasi variabile di modello dichiarata su un lato di un modello del tipo or
non venga assegnata in modo definitivo quando il modello corrisponde. Analogamente, una variabile dichiarata all'interno di uno schema not
non dovrebbe essere assegnata con certezza quando lo schema trova corrispondenza. Il modo più semplice per risolvere questo problema consiste nell'impedire la dichiarazione di variabili di modello in questi contesti. Tuttavia, questo potrebbe essere troppo restrittivo. Esistono altri approcci da considerare.
Uno scenario che vale la pena considerare è questo
if (e is not int i) return;
M(i); // is i definitely assigned here?
Questo non funziona oggi perché, per un is-pattern-expression, le variabili di criterio vengono considerate sicuramente assegnate solo dove il is-pattern-expression è vero ("sicuramente assegnato quando vero").
Mantenere il supporto sarebbe più semplice (dal punto di vista del programmatore) rispetto all'aggiunta del supporto per un'istruzione if
con condizione negata. Anche se si aggiunge tale supporto, i programmatori si chiederebbero perché il frammento di codice precedente non funziona. D'altra parte, lo stesso scenario in un switch
ha meno senso, in quanto non esiste alcun punto corrispondente nel programma in cui è assegnato sicuramente quando è falso sarebbe significativo. Permetteremmo questo in un is-pattern-expression ma non in altri contesti in cui i modelli sono consentiti? Sembra irregolare.
Correlato a questo è il problema dell'assegnazione definita in un modello di disgiuntivo.
if (e is 0 or int i)
{
M(i); // is i definitely assigned here?
}
Ci aspettiamo che i
venga assegnato con certezza solo quando l'input non è zero. Tuttavia, poiché non sappiamo se l'input è zero o meno all'interno del blocco, i
non è sicuramente assegnato. Tuttavia, cosa accade se si consente di dichiarare i
in modelli che si escludono a vicenda?
if ((e1, e2) is (0, int i) or (int i, 0))
{
M(i);
}
In questo caso, la variabile i
è sicuramente assegnata all'interno di questo blocco e prende il suo valore dall'altro elemento della tupla quando si trova un elemento zero.
Si è anche suggerito di permettere che le variabili vengano definite (più volte) in ogni caso di un blocco di casi.
case (0, int x):
case (int x, 0):
Console.WriteLine(x);
Per eseguire una qualsiasi di queste operazioni, è necessario definire attentamente dove tali definizioni sono consentite e in quali condizioni tale variabile viene considerata sicuramente assegnata.
Se decidessimo di rinviare tale lavoro ad un momento successivo (cosa che consiglio), potremmo dire in C# 9.
- sotto un
not
oor
, le variabili di modello potrebbero non essere dichiarate.
Quindi, avremmo il tempo di sviluppare un'esperienza che fornirà informazioni dettagliate sul possibile valore di rilassarsi in seguito.
Risultato: le variabili del modello non possono essere dichiarate sotto un pattern not
o or
.
Diagnostica, sussunzione e completezza
Queste nuove forme di schemi introducono molte nuove opportunità per errori dei programmatori diagnostici. Sarà necessario decidere quali tipi di errori diagnosticare e come eseguire questa operazione. Ecco alcuni esempi:
case >= 0 and <= 100D:
Questo caso non può mai corrispondere perché l'input non può essere sia un int
che un double
. È già presente un errore quando viene rilevato un caso che non potrà mai corrispondere, ma la formulazione ("Il caso switch è già stato gestito da un caso precedente" e "Il modello è già stato gestito da un ramo precedente dell'espressione switch") potrebbe essere fuorviante in nuovi scenari. Potrebbe essere necessario modificare la formulazione per dire semplicemente che il modello non corrisponderà mai all'input.
case 1 and 2:
Analogamente, si tratta di un errore perché un valore non può essere sia 1
che 2
.
case 1 or 2 or 3 or 1:
Questo caso può corrispondere, ma il or 1
alla fine non aggiunge alcun significato al modello. Suggerisco di generare un errore ogni volta che una congiunzione o disgiunzione di uno schema composto non definisce una variabile di schema o non influisce sull'insieme dei valori corrispondenti.
case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;
In questo caso, 0 or 1 or
non aggiunge nulla al secondo caso, in quanto tali valori sarebbero stati gestiti dal primo caso. Anche questo merita un errore.
byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };
Un'espressione switch come questa deve essere considerata esaustiva (gestisce tutti i possibili valori di input).
In C# 8.0, un'espressione switch con un input di tipo byte
viene considerata esaustiva solo se contiene un braccio finale il cui criterio corrisponde a tutto (un discard-pattern o var-pattern). Anche un'espressione switch con un ramo per ogni valore distinto di byte
non è considerata esaustiva in C# 8. Per gestire correttamente l'esaustività dei modelli relazionali, è necessario gestire anche questo caso. Tecnicamente si tratta di una modifica che causa un'interruzione, ma è probabile che nessun utente noti.
C# feature specifications