Discards - Nozioni fondamentali su C#

A partire da C# 7.0, C# supporta le variabili discard, ovvero variabili segnaposto intenzionalmente non utilizzate nel codice dell'applicazione. Le variabili discard sono equivalenti alle variabili non assegnate. non hanno un valore. Una classe discard comunica la finalità al compilatore e ad altri utenti che leggono il codice: si intendeva ignorare il risultato di un'espressione. È possibile ignorare il risultato di un'espressione, uno o più membri di un'espressione di tupla, un parametro per un metodo o la destinazione di un'espressione out di criteri di ricerca.

Poiché è presente una sola variabile discard, è possibile che alla variabile non venga allocata memoria. Le eliminazioni possono ridurre le allocazioni di memoria. Le eliminazioni rendono chiara la finalità del codice. 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 al metodo seguente restituisce una tupla in cui il primo e il secondo valore sono discard. area è una variabile dichiarata in precedenza impostata sul terzo componente restituito da GetCityInformation :

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

A partire da C# 9.0, è possibile usare discards per specificare i parametri di input non usati di un'espressione lambda. Per altre informazioni, vedere la sezione Parametri di input di un'espressione lambda dell'articolo Espressioni lambda.

Quando è una discard valida, il tentativo di recuperarne il valore o di usarlo in un'operazione di assegnazione genera l'errore del compilatore _ CS0301, "Il nome ' ' non esiste nel _ contesto corrente". Questo errore è dovuto al fatto che non viene assegnato un valore e potrebbe anche non _ essere assegnato un percorso di archiviazione. Se si tratta di una variabile effettiva, non è stato possibile rimuovere più di un valore, come nell'esempio precedente.

Decostruzione di tuple e oggetti

Le eliminazioni sono utili nell'uso delle tuple quando il codice dell'applicazione usa alcuni elementi di tupla, ma ne ignora altri. Ad esempio, il metodo seguente restituisce una tupla con il nome di una città, la relativa area, un anno, la popolazione della città per tale anno, un secondo anno e la popolazione della città per il secondo QueryCityDataForYears 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 eliminazioni quando si è interessati a usare solo un subset dei valori decostrutti. 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 discard può essere usato nei criteri di ricerca con l'espressione switch. Ogni espressione, inclusa null , corrisponde sempre al criterio discard.

Nell'esempio seguente viene definito un metodo che usa un'espressione per determinare se un oggetto fornisce un'implementazione e verifica ProvidesFormatInfo switch se IFormatProvider 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 ai metodi con out parametri

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 out argomenti 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 è l'uso di un'assegnazione per assicurarsi che un argomento non sia Null. Nel codice seguente viene utilizzato un oggetto discard per forzare un'assegnazione. Il lato destro dell'assegnazione usa l'operatore null coalescing per generare System.ArgumentNullException un'eccezione quando l'argomento è null . Il codice non richiede il risultato dell'assegnazione, quindi viene eliminato. L'espressione forza un controllo Null. La funzione discard 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 mentre sta per essere completata. La finalità è chiara: si vuole rimuovere Task e ignorare eventuali errori generati dall'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 una classe discard, 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, il debugger arresterà il programma quando viene generata l'eccezione. Senza un debugger collegato, l'eccezione viene ignorata automaticamente 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. 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. 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". Ad 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
    

Vedere anche