Variabili discard - Nozioni fondamentali di C#

Le variabili discard sono variabili segnaposto intenzionalmente inutilizzate nel codice dell'applicazione. Le variabili discard sono equivalenti alle variabili non assegnate. Non hanno un valore. Una variabile discard comunica la finalità al compilatore e agli altri utenti che leggono il codice: si intende ignorare il risultato di un'espressione. È possibile ignorare il risultato di un'espressione, uno o più membri di un'espressione di tupla, un parametro out di un metodo o la destinazione di un'espressione di criteri di ricerca.

Le variabili discard rendono chiara la finalità del codice. Una variabile discard indica che il codice non usa mai la variabile. Migliorano la leggibilità e la manutenibilità.

Per indicare che una variabile è una variabile discard le si assegna come nome il carattere di sottolineatura (_). Ad esempio, la chiamata a un metodo seguente restituisce una tupla nella quale i primi e i secondi valori vengono eliminati. area è una variabile dichiarata in precedenza impostata sul terzo componente restituito da GetCityInformation:

(_, _, area) = city.GetCityInformation(cityName);

È possibile usare le eliminazioni per specificare i parametri di input di un'espressione lambda inutilizzati. Per altre informazioni, vedere la sezione Parametri di input di un'espressione lambda dell'articolo Espressioni lambda.

Quando _ è una variabile discard valida, se si prova a recuperarne il valore o a usarla in un'operazione di assegnazione viene generato l'errore di compilazione CS0103, "Il nome '_' non esiste nel contesto corrente". L'errore si verifica perché a _ non è assegnato nessun valore e potrebbe non essere assegnata nessuna posizione di archiviazione. Se fosse una variabile vera e propria, non sarebbe possibile rimuovere più di un valore, come nell'esempio precedente.

Decostruzione di tuple e oggetti

Le variabili discard sono utili per lavorare con le tuple quando il codice dell'applicazione usa alcuni elementi della tupla ma ne ignora altri. Ad esempio, il metodo QueryCityDataForYears seguente restituisce una tupla con il nome di una città, la relativa area, un anno, la popolazione della città per quell'anno, un secondo anno e la popolazione della città per quel secondo anno. L'esempio visualizza la variazione della popolazione tra questi due anni. Tra i dati resi disponibili dalla tupla non interessa l'area della città, mentre il nome della città e le due date sono già noti in fase di progettazione. Di conseguenza interessano soltanto i due valori di popolazione archiviati nella tupla, mentre gli altri valori possono essere gestiti come variabili discard.

var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");

static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
{
    int population1 = 0, population2 = 0;
    double area = 0;

    if (name == "New York City")
    {
        area = 468.48;
        if (year1 == 1960)
        {
            population1 = 7781984;
        }
        if (year2 == 2010)
        {
            population2 = 8175133;
        }
        return (name, area, year1, population1, year2, population2);
    }

    return ("", 0, 0, 0, 0, 0);
}
// The example displays the following output:
//      Population change, 1960 to 2010: 393,149

Per altre informazioni sulla decostruzione di tuple con le variabili discard, vedere Decostruzione di tuple e altri tipi.

Anche il metodo Deconstruct di una classe, struttura o interfaccia consente di recuperare e decostruire un set di dati specifico da un oggetto. È possibile usare le variabili discard quando si vuole lavorare solo con un subset dei valori decostruiti. L'esempio seguente esegue la decostruzione di un oggetto Person in quattro stringhe (nome, cognome, città e stato), ma rimuove il cognome e lo stato.

using System;

namespace Discards
{
    public class Person
    {
        public string FirstName { get; set; }
        public string MiddleName { get; set; }
        public string LastName { get; set; }
        public string City { get; set; }
        public string State { get; set; }

        public Person(string fname, string mname, string lname,
                      string cityName, string stateName)
        {
            FirstName = fname;
            MiddleName = mname;
            LastName = lname;
            City = cityName;
            State = stateName;
        }

        // Return the first and last name.
        public void Deconstruct(out string fname, out string lname)
        {
            fname = FirstName;
            lname = LastName;
        }

        public void Deconstruct(out string fname, out string mname, out string lname)
        {
            fname = FirstName;
            mname = MiddleName;
            lname = LastName;
        }

        public void Deconstruct(out string fname, out string lname,
                                out string city, out string state)
        {
            fname = FirstName;
            lname = LastName;
            city = City;
            state = State;
        }
    }
    class Example
    {
        public static void Main()
        {
            var p = new Person("John", "Quincy", "Adams", "Boston", "MA");

            // Deconstruct the person object.
            var (fName, _, city, _) = p;
            Console.WriteLine($"Hello {fName} of {city}!");
            // The example displays the following output:
            //      Hello John of Boston!
        }
    }
}

Per altre informazioni sulla decostruzione di tipi definiti dall'utente con le variabili discard, vedere Decostruzione di tuple e altri tipi.

Criteri di ricerca con switch

Il criterio di rimozione può essere usato nei criteri di ricerca con l'espressione switch. Ogni espressione, incluso null, corrisponde sempre al criterio di rimozione.

L'esempio seguente definisce un metodo ProvidesFormatInfo che usa un'espressione switch per determinare se un oggetto include un'implementazione IFormatProvider e verifica se l'oggetto è null. Usa anche il criterio variabile discard per gestire gli oggetti non null di qualsiasi altro tipo.

object?[] objects = [CultureInfo.CurrentCulture,
                   CultureInfo.CurrentCulture.DateTimeFormat,
                   CultureInfo.CurrentCulture.NumberFormat,
                   new ArgumentException(), null];
foreach (var obj in objects)
    ProvidesFormatInfo(obj);

static void ProvidesFormatInfo(object? obj) =>
    Console.WriteLine(obj switch
    {
        IFormatProvider fmt => $"{fmt.GetType()} object",
        null => "A null object reference: Its use could result in a NullReferenceException",
        _ => "Some object type without format information"
    });
// The example displays the following output:
//    System.Globalization.CultureInfo object
//    System.Globalization.DateTimeFormatInfo object
//    System.Globalization.NumberFormatInfo object
//    Some object type without format information
//    A null object reference: Its use could result in a NullReferenceException

Chiamate a metodi con parametri out

Quando si chiama il metodo Deconstruct per la decostruzione di un tipo definito dall'utente (un'istanza di una classe, una struttura o un'interfaccia), è possibile rimuovere i valori di singoli argomenti out. Tuttavia, è anche possibile rimuovere il valore degli argomenti out quando si chiama qualsiasi metodo con un parametro out.

Nel seguente esempio viene chiamato il metodo DateTime.TryParse(String, out DateTime) per determinare se la rappresentazione stringa di una data è valida con le impostazioni cultura correnti. Dato che lo scopo dell'esempio è solo quello di convalidare la stringa di data e non quello di analizzarla per estrarre la data, l'argomento out del metodo è una variabile discard.

string[] dateStrings = ["05/01/2018 14:57:32.8", "2018-05-01 14:57:32.8",
                      "2018-05-01T14:57:32.8375298-04:00", "5/01/2018",
                      "5/01/2018 14:57:32.80 -07:00",
                      "1 May 2018 2:57:32.8 PM", "16-05-2018 1:00:32 PM",
                      "Fri, 15 May 2018 20:10:57 GMT"];
foreach (string dateString in dateStrings)
{
    if (DateTime.TryParse(dateString, out _))
        Console.WriteLine($"'{dateString}': valid");
    else
        Console.WriteLine($"'{dateString}': invalid");
}
// The example displays output like the following:
//       '05/01/2018 14:57:32.8': valid
//       '2018-05-01 14:57:32.8': valid
//       '2018-05-01T14:57:32.8375298-04:00': valid
//       '5/01/2018': valid
//       '5/01/2018 14:57:32.80 -07:00': valid
//       '1 May 2018 2:57:32.8 PM': valid
//       '16-05-2018 1:00:32 PM': invalid
//       'Fri, 15 May 2018 20:10:57 GMT': invalid

Una variabile discard standalone

È possibile usare una variabile discard standalone per indicare qualsiasi variabile che si è deciso di ignorare. Un uso tipico è quello di usare un assegnazione per assicurarsi che un argomento non sia Null. Il codice seguente usa un'istruzione discard per forzare un'assegnazione. Il lato destro dell'assegnazione usa l'operatore di unione Null per generare un System.ArgumentNullException quando l'argomento è null. Il codice non richiede il risultato dell'assegnazione, quindi viene rimosso. L'espressione forza un controllo Null. La rimozione chiarisce la finalità: il risultato dell'assegnazione non è necessario o usato.

public static void Method(string arg)
{
    _ = arg ?? throw new ArgumentNullException(paramName: nameof(arg), message: "arg can't be null");

    // Do work with arg.
}

Nell'esempio seguente viene usata una variabile discard standalone per ignorare l'oggetto Task restituito da un'operazione asincrona. L'assegnazione dell'attività ha l'effetto di eliminare l'eccezione generata dall'operazione quando sta per essere completata. Rende chiara la finalità: si vuole eliminare Taske ignorare eventuali errori generati da tale operazione asincrona.

private static async Task ExecuteAsyncMethods()
{
    Console.WriteLine("About to launch a task...");
    _ = Task.Run(() =>
    {
        var iterations = 0;
        for (int ctr = 0; ctr < int.MaxValue; ctr++)
            iterations++;
        Console.WriteLine("Completed looping operation...");
        throw new InvalidOperationException();
    });
    await Task.Delay(5000);
    Console.WriteLine("Exiting after 5 second delay");
}
// The example displays output like the following:
//       About to launch a task...
//       Completed looping operation...
//       Exiting after 5 second delay

Senza assegnare l'attività a un'operazione di rimozione, il codice seguente genera un avviso del compilatore:

private static async Task ExecuteAsyncMethods()
{
    Console.WriteLine("About to launch a task...");
    // CS4014: Because this call is not awaited, execution of the current method continues before the call is completed.
    // Consider applying the 'await' operator to the result of the call.
    Task.Run(() =>
    {
        var iterations = 0;
        for (int ctr = 0; ctr < int.MaxValue; ctr++)
            iterations++;
        Console.WriteLine("Completed looping operation...");
        throw new InvalidOperationException();
    });
    await Task.Delay(5000);
    Console.WriteLine("Exiting after 5 second delay");

Nota

Se si esegue uno dei due esempi precedenti usando un debugger, quest'ultimo interromperà il programma quando viene generata l'eccezione. Senza un debugger collegato, l'eccezione viene ignorata in entrambi i casi.

Anche _ è un identificatore valido. Quando viene usata fuori da un contesto supportato, _ non viene considerata come una variabile discard ma come una variabile valida. Se un identificatore con nome _ è già incluso nell'ambito, l'uso di _ come variabile discard standalone può causare:

  • La modifica accidentale della variabile _ dell'ambito, alla quale viene assegnato il valore della variabile discard prevista. Ad esempio:
    private static void ShowValue(int _)
    {
       byte[] arr = [0, 0, 1, 2];
       _ = BitConverter.ToInt32(arr, 0);
       Console.WriteLine(_);
    }
     // The example displays the following output:
     //       33619968
    
  • Un errore del compilatore per la violazione dell'indipendenza dai tipi. Ad esempio:
    private static bool RoundTrips(int _)
    {
       string value = _.ToString();
       int newValue = 0;
       _ = Int32.TryParse(value, out newValue);
       return _ == newValue;
    }
    // The example displays the following compiler error:
    //      error CS0029: Cannot implicitly convert type 'bool' to 'int'
    
  • Errore del compilatore CS0136: "Non è possibile dichiarare in questo ambito una variabile locale o un parametro denominato '_' perché tale nome viene usato in un ambito locale di inclusione per definire una variabile locale o un parametro". Esempio:
     public void DoSomething(int _)
    {
     var _ = GetValue(); // Error: cannot declare local _ when one is already in scope
    }
    // The example displays the following compiler error:
    // error CS0136:
    //       A local or parameter named '_' cannot be declared in this scope
    //       because that name is used in an enclosing local scope
    //       to define a local or parameter
    

Vedi anche