Descartes: aspectos básicos de C#

Los descartes son variables de marcador de posición que deliberadamente no se usan en el código de la aplicación. Los descartes son equivalentes a variables sin asignar, ya que no tienen un valor. Un descarte comunica la intención al compilador y otros usuarios que leen el código: Pretendía ignorar el resultado de una expresión. Es posible que desee ignorar el resultado de una expresión, uno o varios miembros de una expresión de tupla, un parámetro out de un método o el destino de una expresión de coincidencia de patrones.

Los descartes aclaran la intención del código. Un descarte indica que el código nunca usa la variable. Mejoran la legibilidad y el mantenimiento.

Para indicar que una variable es un descarte, se le asigna como nombre el carácter de subrayado (_). Por ejemplo, la siguiente llamada de método devuelve una tupla en la que el primer y el segundo valor son descartes. area es una variable declarada previamente establecida en el tercer componente devuelto por GetCityInformation:

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

Puede usar descartes para especificar parámetros de entrada sin usar de una expresión lambda. Para más información, consulte sección sobre parámetros de entrada de una expresión lambda en el artículo sobre expresiones lambda.

Cuando _ es un descarte válido, si se intenta recuperar su valor o usarlo en una operación de asignación, se genera el error del compilador CS0103: "El nombre '_' no existe en el contexto actual". Este error se debe a que no se le ha asignado un valor a _, y es posible que tampoco se le haya asignado una ubicación de almacenamiento. Si fuese una variable real no se podría descartar más de un valor, como en el ejemplo anterior.

Deconstrucción de tuplas y objetos

Los descartes son útiles en el trabajo con tuplas, cuando el código de la aplicación usa algunos elementos de tupla pero omite otros. Por ejemplo, el siguiente método QueryCityDataForYears devuelve una tupla con el nombre de una ciudad, su superficie, un año, la población de la ciudad en ese año, un segundo año y la población de la ciudad en ese segundo año. En el ejemplo se muestra la evolución de la población entre esos dos años. De los datos disponibles en la tupla, no nos interesa la superficie de la ciudad, y conocemos el nombre de la ciudad y las dos fechas en tiempo de diseño. Como resultado, solo nos interesan los dos valores de población almacenados en la tupla, y podemos controlar los 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 obtener más información sobre la deconstrucción de tuplas con descartes, vea Deconstructing tuples and other types (Deconstruir tuplas y otros tipos).

El método Deconstruct de una clase, estructura o interfaz también permite recuperar y deconstruir un conjunto de datos específico de un objeto. Puede usar descartes cuando le interese trabajar con un solo subconjunto de los valores deconstruidos. En el siguiente ejemplo se deconstruye un objeto Person en cuatro cadenas (el nombre propio, los apellidos, la ciudad y el estado), pero se descartan los apellidos y el 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 obtener más información sobre la deconstrucción de tipos definidos por el usuario con descartes, vea Deconstructing tuples and other types (Deconstruir tuplas y otros tipos).

Coincidencia de patrones con switch

El patrón de descarte se puede usar en la coincidencia de patrones con la expresión switch. Todas las expresiones, incluida null, siempre coinciden con el patrón de descarte.

En el ejemplo siguiente se define un método ProvidesFormatInfo que usa una expresión switch para determinar si un objeto proporciona una implementación de IFormatProvider y probar si el objeto es null. También se usa el patrón de descarte para controlar los objetos que no son NULL de cualquier otro 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

Llamadas a métodos con parámetros out

Cuando se llama al método Deconstruct para deconstruir un tipo definido por el usuario (una instancia de una clase, estructura o interfaz), puede descartar los valores de argumentos out individuales. Pero también puede descartar el valor de argumentos out al llamar a cualquier método con un parámetro out.

En el ejemplo siguiente se llama al método DateTime.TryParse(String, out DateTime) para determinar si la representación de cadena de una fecha es válida en la referencia cultural actual. Dado que al ejemplo solo le interesa validar la cadena de fecha, y no analizarla para extraer la fecha, el argumento out para el método es un 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

Descarte independiente

Puede usar un descarte independiente para indicar cualquier variable que decida omitir. Un uso típico es usar una asignación para asegurarse de que un argumento no sea NULL. En el código siguiente se usa un descarte para forzar una asignación. El lado derecho de la asignación utiliza el operador de uso combinado de NULL para producir System.ArgumentNullException cuando el argumento es null. El código no necesita el resultado de la asignación, por lo que se descarta. La expresión fuerza una comprobación nula. El descarte aclara su intención: el resultado de la asignación no es necesario ni se usa.

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

    // Do work with arg.
}

En el ejemplo siguiente se usa un descarte independiente para omitir el objeto Task devuelto por una operación asincrónica. La asignación de la tarea tiene el efecto de suprimir la excepción que se produce en la operación cuando está a punto de completarse. Hace que su intención sea clara: Quiere descartar Task y omitir los errores generados a partir de esa operación asincrónica.

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

Sin asignar la tarea a un descarte, el código siguiente genera una advertencia del 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");

Nota

Si ejecuta cualquiera de los dos ejemplos anteriores mediante un depurador, este detendrá el programa cuando se produzca la excepción. Sin un depurador asociado, la excepción se omite en ambos casos en modo silencioso.

_ también es un identificador válido. Cuando se usa fuera de un contexto compatible, _ no se trata como un descarte, sino como una variable válida. Si un identificador denominado _ ya está en el ámbito, el uso de _ como descarte independiente puede producir lo siguiente:

  • La modificación accidental del valor de la variable _ en el ámbito, al asignarle el valor del descarte previsto. Por ejemplo:
    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 error del compilador por infringir la seguridad de tipos. Por ejemplo:
    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'
    
  • Error del compilador CS0136: "Una variable local o un parámetro denominados '_' no se pueden declarar en este ámbito porque ese nombre se está usando en un ámbito local envolvente para definir una variable local o un parámetro". Por ejemplo:
     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
    

Consulte también