Panoramica dei criteri di ricerca

Criteri di ricerca è una tecnica in cui si testa un'espressione per determinare se presenta determinate caratteristiche. I criteri di ricerca C# forniscono una sintassi più concisa per testare le espressioni e intervenire quando un'espressione corrisponde. L'espressione "expression is" supporta la corrispondenza dei criteri per testare un'espressione e dichiarare in modo condizionale una nuova variabile al risultato di tale espressione. L'”espressione switch" consente di eseguire azioni in base al primo criterio di corrispondenza per un'espressione. Queste due espressioni supportano un vocabolario avanzato di modelli.

Questo articolo offre una panoramica degli scenari in cui è possibile usare i criteri di ricerca. Queste tecniche possono migliorare la leggibilità e la correttezza del codice. Per una descrizione completa di tutti i modelli che è possibile applicare, vedere l'articolo sui modelli nelle informazioni di riferimento sul linguaggio.

Controlli Null

Uno degli scenari più comuni per la corrispondenza dei criteri di ricerca consiste nel garantire che i valori non siano null. È possibile testare e convertire un tipo valore nullable nel tipo sottostante durante il test per null usando l'esempio seguente:

int? maybe = 12;

if (maybe is int number)
{
    Console.WriteLine($"The nullable int 'maybe' has the value {number}");
}
else
{
    Console.WriteLine("The nullable int 'maybe' doesn't hold a value");
}

Il codice precedente è un modello di dichiarazione per testare il tipo della variabile e assegnarlo a una nuova variabile. Le regole del linguaggio rendono questa tecnica più sicura di molte altre. La variabile number è accessibile e assegnata solo nella parte vera della clausola if. Se si tenta di accedervi altrove, nella clausola else o dopo il blocco if, il compilatore genera un errore. In secondo luogo, poiché non si usa l'operatore ==, questo modello funziona quando un tipo esegue l'overload dell'operatore ==. Questo lo rende un modo ideale per controllare i valori di riferimento Null, aggiungendo il modello di not:

string? message = ReadMessageOrDefault();

if (message is not null)
{
    Console.WriteLine(message);
}

Nell'esempio precedente è stato usato un modello costante per confrontare la variabile con null. not è un modello logico che corrisponde quando il criterio negato non corrisponde.

Test dei tipi

Un altro uso comune per i criteri di ricerca consiste nel testare una variabile per verificare se corrisponde a un determinato tipo. Ad esempio, il codice seguente verifica se una variabile non è Null e implementa l'interfaccia System.Collections.Generic.IList<T>. In caso affermativo, usa la proprietà ICollection<T>.Count in tale elenco per trovare l'indice intermedio. Il modello di dichiarazione non corrisponde a un valore null, indipendentemente dal tipo in fase di compilazione della variabile. Il codice seguente protegge da null, oltre a proteggere da un tipo che non implementa IList.

public static T MidPoint<T>(IEnumerable<T> sequence)
{
    if (sequence is IList<T> list)
    {
        return list[list.Count / 2];
    }
    else if (sequence is null)
    {
        throw new ArgumentNullException(nameof(sequence), "Sequence can't be null.");
    }
    else
    {
        int halfLength = sequence.Count() / 2 - 1;
        if (halfLength < 0) halfLength = 0;
        return sequence.Skip(halfLength).First();
    }
}

Gli stessi test possono essere applicati in un'espressione switch per testare una variabile su più tipi diversi. È possibile usare queste informazioni per creare algoritmi migliori in base al tipo di runtime specifico.

Confrontare valori discreti

È anche possibile testare una variabile per trovare una corrispondenza su valori specifici. Il codice seguente mostra un esempio in cui si testa un valore su tutti i valori possibili dichiarati in un'enumerazione:

public State PerformOperation(Operation command) =>
   command switch
   {
       Operation.SystemTest => RunDiagnostics(),
       Operation.Start => StartSystem(),
       Operation.Stop => StopSystem(),
       Operation.Reset => ResetToReady(),
       _ => throw new ArgumentException("Invalid enum value for command", nameof(command)),
   };

Nell'esempio precedente viene illustrato un dispatch del metodo in base al valore di un'enumerazione. Il caso _ finale è un criterio discard che corrisponde a tutti i valori. Gestisce tutte le condizioni di errore in cui il valore non corrisponde a uno dei valori definiti enum. Se si omette tale braccio switch, il compilatore avvisa che l'espressione di criteri non gestisce tutti i valori di input possibili. In fase di esecuzione, l'espressione genera un'eccezione switch se l'oggetto esaminato non corrisponde a nessuna delle braccia del commutatore. È possibile usare costanti numeriche anziché un set di valori di enumerazione. È anche possibile usare questa tecnica simile per i valori stringa costanti che rappresentano i comandi:

public State PerformOperation(string command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

L'esempio precedente mostra lo stesso algoritmo, ma usa valori stringa anziché un'enumerazione. Questo scenario viene usato se l'applicazione risponde ai comandi di testo anziché a un formato di dati normale. A partire da C# 11, è anche possibile usare Span<char> o ReadOnlySpan<char> per testare i valori stringa costanti, come illustrato nell'esempio seguente:

public State PerformOperation(ReadOnlySpan<char> command) =>
   command switch
   {
       "SystemTest" => RunDiagnostics(),
       "Start" => StartSystem(),
       "Stop" => StopSystem(),
       "Reset" => ResetToReady(),
       _ => throw new ArgumentException("Invalid string value for command", nameof(command)),
   };

In tutti questi esempi, il modello di eliminazione garantisce la gestione di ogni input. Il compilatore consente di assicurarsi che ogni valore di input possibile venga gestito.

Modelli relazionali

È possibile usare modellirelazionali per testare il modo in cui un valore viene confrontato con le costanti. Ad esempio, il codice seguente restituisce lo stato dell'acqua in base alla temperatura in Fahrenheit:

string WaterState(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        (> 32) and (< 212) => "liquid",
        < 32 => "solid",
        > 212 => "gas",
        32 => "solid/liquid transition",
        212 => "liquid / gas transition",
    };

Il codice precedente illustra anche il modello logico and congiuntivo per verificare che entrambi i modelli relazionali corrispondano. È anche possibile usare un modello di or disgiuntivo per verificare che uno dei due criteri corrisponda. I due modelli relazionali sono racchiusi tra parentesi, che è possibile usare intorno a qualsiasi modello per maggiore chiarezza. Le ultime due braccia switch gestiscono i casi per il punto di fusione e il punto di ebollizione. Senza queste due braccia, il compilatore avvisa che la logica non copre ogni possibile input.

Il codice precedente illustra anche un'altra importante funzionalità fornita dal compilatore per le espressioni di ricerca di criteri: il compilatore avvisa se non si gestisce ogni valore di input. Il compilatore genera anche un avviso se il criterio relativo a un braccio switch è coperto da un criterio precedente. In questo modo è possibile effettuare il refactoring e riordinare le espressioni switch. Un altro modo per scrivere la stessa espressione potrebbe essere:

string WaterState2(int tempInFahrenheit) =>
    tempInFahrenheit switch
    {
        < 32 => "solid",
        32 => "solid/liquid transition",
        < 212 => "liquid",
        212 => "liquid / gas transition",
        _ => "gas",
};

La lezione fondamentale appresa nell'esempio precedente, e in qualsiasi altro refactoring o riordinamento, è che il compilatore verifica che il codice gestisca tutti gli input possibili.

Input multipli

Con tutti i criteri trattati finora è stato controllato un solo input. È possibile scrivere modelli che esaminano più proprietà di un oggetto. Considerare il record di Order seguente:

public record Order(int Items, decimal Cost);

Il tipo di record posizionale precedente dichiara due membri in posizioni esplicite. Appare prima Items, poi il tipo Cost dell'ordine. Per altre informazioni, vedere Record.

Il codice seguente esamina il numero di articoli e il valore di un ordine per calcolare un prezzo scontato:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        { Items: > 10, Cost: > 1000.00m } => 0.10m,
        { Items: > 5, Cost: > 500.00m } => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

Le prime due braccia esaminano due proprietà dell'oggetto Order. Il terzo esamina solo il costo. Il controllo successivo viene eseguito su null,e l’ultimo corrisponde a qualsiasi altro valore. Se il tipo Order definisce un metodo di Deconstruct appropriato, è possibile omettere i nomi delle proprietà dal modello e usare la decostruzione per esaminare le proprietà:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        ( > 10,  > 1000.00m) => 0.10m,
        ( > 5, > 50.00m) => 0.05m,
        { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

Il codice precedente illustra il modello posizionale in cui le proprietà vengono decostruite per l'espressione.

Modelli elenco

È possibile controllare gli elementi in un elenco o in una matrice usando un criterio di elenco. Un criterio di elenco consente di applicare un criterio a qualsiasi elemento di una sequenza. Inoltre, è possibile applicare il criterio di eliminazione (_) per trovare la corrispondenza con qualsiasi elemento o applicare un criterio di sezione in modo che corrisponda a zero o più elementi.

I modelli di elenco sono uno strumento utile quando i dati non seguono una struttura regolare. È possibile usare criteri di ricerca per testare la forma e i valori dei dati anziché trasformarli in un set di oggetti.

Si consideri l'estratto seguente da un file di testo contenente transazioni bancarie:

04-01-2020, DEPOSIT,    Initial deposit,            2250.00
04-15-2020, DEPOSIT,    Refund,                      125.65
04-18-2020, DEPOSIT,    Paycheck,                    825.65
04-22-2020, WITHDRAWAL, Debit,           Groceries,  255.73
05-01-2020, WITHDRAWAL, #1102,           Rent, apt, 2100.00
05-02-2020, INTEREST,                                  0.65
05-07-2020, WITHDRAWAL, Debit,           Movies,      12.57
04-15-2020, FEE,                                       5.55

Si tratta di un formato CSV, ma alcune delle righe hanno più colonne di altre. Ancora peggio per l'elaborazione, una colonna nel tipo WITHDRAWAL contiene testo generato dall'utente e può contenere una virgola nel testo. Un criterio elenco che include il criterio di eliminazione, il criterio di costante e il criterio var per acquisire il valore elabora i dati in questo formato:

decimal balance = 0m;
foreach (string[] transaction in ReadRecords())
{
    balance += transaction switch
    {
        [_, "DEPOSIT", _, var amount]     => decimal.Parse(amount),
        [_, "WITHDRAWAL", .., var amount] => -decimal.Parse(amount),
        [_, "INTEREST", var amount]       => decimal.Parse(amount),
        [_, "FEE", var fee]               => -decimal.Parse(fee),
        _                                 => throw new InvalidOperationException($"Record {string.Join(", ", transaction)} is not in the expected format!"),
    };
    Console.WriteLine($"Record: {string.Join(", ", transaction)}, New balance: {balance:C}");
}

L'esempio precedente accetta una matrice di stringhe, in cui ogni elemento è un campo nella riga. Le chiavi dell'espressione switch nel secondo campo, che determina il tipo di transazione e il numero di colonne rimanenti. Ogni riga garantisce che i dati siano nel formato corretto. Il criterio di eliminazione (_) ignora il primo campo, con la data della transazione. Il secondo campo corrisponde al tipo di transazione. Gli elementi rimanenti corrispondono al campo con la quantità. La corrispondenza finale usa il modello var per acquisire la rappresentazione di stringa dell'importo. L'espressione calcola l'importo da aggiungere o sottrarre dal saldo.

I modelli di elenco consentono di trovare una corrispondenza nella forma di una sequenza di elementi dati. Usare i modelli di eliminazione e sezione per trovare la corrispondenza con la posizione degli elementi. Si usano altri modelli per trovare corrispondenze con le caratteristiche relative ai singoli elementi.

Questo articolo ha fornito una panoramica dei tipi di codice che è possibile scrivere con criteri di ricerca in C#. Gli articoli seguenti illustrano altri esempi di utilizzo dei modelli negli scenari e il vocabolario completo dei modelli disponibili per l'uso.

Vedi anche