Descartes – conceitos básicos do C#

Descartes são variáveis de espaço reservado intencionalmente não utilizadas no código do aplicativo. Descartes são equivalentes a variáveis não atribuídas; eles não têm um valor. Um descarte comunica uma intenção para o compilador e outras pessoas que leem seu código: você pretendia ignorar o resultado de uma expressão. Talvez você queira ignorar o resultado de uma expressão, um ou mais membros de uma expressão de tupla, um parâmetro out para um método ou o destino de uma expressão de correspondência de padrões.

Os descartes tornam a intenção do seu código clara. Um descarte indica que nosso código nunca usa a variável. Eles aprimoram sua legibilidade e manutenção.

Você indica que uma variável é um descarte atribuindo a ela o sublinhado (_) como seu nome. Por exemplo, a chamada de método a seguir retorna uma tupla na qual o primeiro e o segundo valores são descartes. area é uma variável declarada anteriormente definida como o terceiro componente retornado por GetCityInformation:

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

Você pode usar discards para especificar parâmetros de entrada não utilizados de uma expressão lambda. Para obter mais informações, consulte a seção Parâmetros de entrada de uma expressão lambda do artigo Expressões lambda.

Quando _ é um descarte válido, tentar recuperar seu valor ou usá-lo em uma operação de atribuição gerará o erro do compilador CS0103, "O nome '_' não existe no contexto atual". Esse erro ocorre porque não há um valor atribuído a _, e pode não haver nem mesmo um local de armazenamento atribuído a ela. Se ela fosse uma variável real, você não poderia descartar mais de um valor, tal como ocorreu no exemplo anterior.

Desconstrução de objeto e de tupla

Descartes são úteis para trabalhar com tuplas quando seu código de aplicativo usa alguns elementos da tupla, mas ignora outros. Por exemplo, o método QueryCityDataForYears a seguir retorna uma tupla de com o nome de uma cidade, sua área, um ano, a população da cidade nesse ano, um segundo ano e população da cidade nesse segundo ano. O exemplo mostra a alteração na população entre esses dois anos. Entre os dados disponíveis da tupla, não estamos preocupados com a área da cidade e sabemos o nome da cidade e as duas datas em tempo de design. Como resultado, estamos interessados apenas nos dois valores de população armazenados na tupla e podemos lidar com seus valores restantes como descartes.

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

Para obter mais informações sobre desconstruir tuplas com descartes, consulte Desconstruindo tuplas e outros tipos.

O método Deconstruct de uma classe, estrutura ou interface também permite que você recupere e decomponha um conjunto específico de dados de um objeto. Você poderá usar descartes quando estiver interessado em trabalhar com apenas um subconjunto dos valores desconstruídos. O exemplo a seguir desconstrói um objeto Person em quatro cadeias de caracteres (os nomes e sobrenomes, a cidade e o estado), mas descarta o sobrenome e o estado.

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!
        }
    }
}

Para obter mais informações sobre desconstruir tipos definidos pelo usuário com descartes, consulte Desconstruindo tuplas e outros tipos.

Correspondência de padrões com switch

O padrão de descarte pode ser usado na correspondência de padrões com a expressão switch. Toda expressão, incluindo null, sempre corresponde ao padrão de descarte.

O exemplo a seguir define um método ProvidesFormatInfo que usa uma expressão switchpara determinar se um objeto fornece uma implementação de IFormatProvider e testa se o objeto é null. Ele também usa o padrão de descarte para manipular objetos não nulos de qualquer outro 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

Chamadas para métodos com parâmetros out

Ao chamar o método Deconstruct para desconstruir um tipo definido pelo usuário (uma instância de uma classe, estrutura ou interface), você pode descartar os valores de argumentos out individuais. Mas você também pode descartar o valor de argumentos out ao chamar qualquer método com um parâmetro out.

A exemplo a seguir chama o método DateTime.TryParse(String, out DateTime) para determinar se a representação de cadeia de caracteres de uma data é válida na cultura atual. Já que o exemplo está preocupado apenas em validar a cadeia de caracteres de data e não em analisá-lo para extrair a data, o argumento out para o método é um descarte.

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

Um descarte autônomo

Você pode usar um descarte autônomo para indicar qualquer variável que você opte por ignorar. Um uso típico é usar uma atribuição para garantir que um argumento não seja nulo. O código a seguir usa um descarte para forçar uma atribuição. O lado direito da atribuição usa o operador de avaliação de nulo para lançar um System.ArgumentNullException quando o argumento é null. O código não precisa do resultado da atribuição; portanto, ele é descartado. A expressão força uma verificação de nulo. O descarte esclarece sua intenção: o resultado da atribuição não é necessário ou usado.

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

    // Do work with arg.
}

O exemplo a seguir usa um descarte autônomo para ignorar o objeto Task retornado por uma operação assíncrona. A atribuição da tarefa tem o efeito de suprimir a exceção que a operação gera quando está prestes a ser concluída. Isso deixa clara sua intenção: você deseja descartar Task e ignorar todos os erros gerados a partir dessa operação assíncrona.

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

Sem atribuir a tarefa a um descarte, o código a seguir gera um aviso do compilador:

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");

Observação

Se você executar um dos dois exemplos anteriores usando um depurador, o depurador interromperá o programa quando a exceção for lançada. Sem um depurador anexado, a exceção é silenciosamente ignorada em ambos os casos.

_ também é um identificador válido. Quando usado fora de um contexto com suporte, _ não é tratado como um descarte, mas como uma variável válida. Se um identificador chamado _ já está no escopo, o uso de _ como um descarte autônomo pode resultar em:

  • A modificação acidental do valor da variável _ no escopo atribuindo a ela o valor do descarte pretendido. Por exemplo:
    private static void ShowValue(int _)
    {
       byte[] arr = [0, 0, 1, 2];
       _ = BitConverter.ToInt32(arr, 0);
       Console.WriteLine(_);
    }
     // The example displays the following output:
     //       33619968
    
  • Um erro do compilador por violação de segurança de tipo. Por exemplo:
    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'
    
  • Erro do compilador CS0136, "Um local ou um parâmetro denominado '_' não pode ser declarado neste escopo porque esse nome é usado em um escopo delimitador de local para definir um local ou parâmetro." Por exemplo:
     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
    

Confira também