Panoramica dei criteri di ricerca

I criteri di ricerca sono una tecnica in cui si testa un'espressione per determinare se ha determinate caratteristiche. I criteri di ricerca C# forniscono una sintassi più concisa per testare le espressioni ed eseguire azioni quando un'espressione corrisponde. is"expression" supporta i criteri di ricerca per testare un'espressione e dichiarare in modo condizionale una nuova variabile al risultato di tale espressione. switch"expression" consente di eseguire azioni in base al primo criterio di corrispondenza per un'espressione. Queste due espressioni supportano un vocabolario ricco 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 i criteri di ricerca è garantire che i valori non siano null . È possibile testare e convertire un tipo valore nullable nel tipo sottostante durante il test null per l'uso dell'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 true della if clausola . Se si tenta di accedervi altrove, nella clausola o dopo il blocco , il compilatore else if 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 not modello:

string? message = "This is not the null string";

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

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

Test dei tipi

Un altro uso comune per i criteri di ricerca è testare una variabile per verificare se corrisponde a un determinato tipo. Ad esempio, il codice seguente verifica se una variabile è non Null e implementa System.Collections.Generic.IList<T> l'interfaccia . In caso contrario, usa la ICollection<T>.Count proprietà nell'elenco per trovare l'indice intermedio. Il modello di dichiarazione non corrisponde a un valore, indipendentemente null dal tipo in fase di compilazione della variabile. Il codice seguente protegge da , oltre a proteggersi da null 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 switch un'espressione per testare una variabile su più tipi diversi. È possibile usare queste informazioni per creare algoritmi migliori in base al tipo di run-time specifico.

Confrontare valori discreti

È anche possibile testare una variabile per trovare una corrispondenza in valori specifici. Il codice seguente illustra un esempio in cui si testa un valore rispetto a 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 di metodo basato sul valore di un'enumerazione . Il caso _ finale è un criterio discard che corrisponde a tutti i valori. Gestisce le condizioni di errore in cui il valore non corrisponde a uno dei valori enum definiti. Se si omette l'opzione arm, il compilatore avvisa che non sono stati gestiti tutti i valori di input possibili. In fase di esecuzione, l'espressione genera un'eccezione se l'oggetto esaminato non corrisponde ad alcuno dei bracci switch di commutazione. È 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)),
   };

Nell'esempio precedente viene illustrato lo stesso algoritmo, ma vengono utilizzati valori stringa anziché un enum. Questo scenario viene utilizzato se l'applicazione risponde a comandi di testo anziché a un normale formato dati. In tutti questi esempi, il modello discard garantisce la gestione di ogni input. Il compilatore consente di assicurarsi che tutti i possibili valori di input siano gestiti.

Modelli relazionali

È possibile usare modelli relazionali per testare il confronto di un valore 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 congiuntivo per verificare che entrambi and i modelli relazionali corrispondano. È anche possibile usare un criterio disgiuntivo or per verificare che entrambi i criteri corrispondano. I due modelli relazionali sono racchiusi tra parentesi, che è possibile usare intorno a qualsiasi modello per maggiore chiarezza. Le due mani di cambio finali gestiscono i case per il punto di fusione e il punto di ebollizione. Senza questi due elementi, il compilatore avvisa che la logica non copre tutti gli input possibili.

Il codice precedente illustra anche un'altra funzionalità importante fornita dal compilatore per le espressioni di criteri di ricerca: il compilatore avvisa se non si gestisce ogni valore di input. Il compilatore genera anche un avviso se un'opzione arm è già gestita da un'opzione arm precedente. In questo modo è possibile eseguire il refactoring e riordinare le espressioni switch. Un altro modo per scrivere la stessa espressione può essere:

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

La lezione chiave di questo e di qualsiasi altro refactoring o riordinamento è che il compilatore convalida che tutti gli input sono stati trattati.

Input multipli

Tutti i modelli visti finora hanno controllato un input. È possibile scrivere modelli che esaminano più proprietà di un oggetto. Si consideri il Order record seguente:

public record Order(int Items, decimal Cost);

Il tipo di record posizionale precedente dichiara due membri in posizioni esplicite. Il primo elemento visualizzato è Items , quindi l'oggetto dell'ordine. Cost Per altre informazioni, vedere Record.

Il codice seguente esamina il numero di elementi 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,
        Order { Cost: > 250.00m } => 0.02m,
        null => throw new ArgumentNullException(nameof(order), "Can't calculate discount on null order"),
        var someObject => 0m,
    };

I primi due bracci esaminano due proprietà dell'oggetto Order . Il terzo esamina solo il costo. Il successivo esegue il controllo null rispetto a e l'oggetto finale corrisponde a qualsiasi altro valore. Se il tipo definisce un metodo appropriato, è possibile omettere i nomi delle proprietà dal modello e usare Order Deconstruct la decostruzione per esaminare le proprietà:

public decimal CalculateDiscount(Order order) =>
    order switch
    {
        ( > 10,  > 1000.00m) => 0.10m,
        ( > 5, > 50.00m) => 0.05m,
        Order { 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.

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

Vedi anche