弃元 - C# 基础知识

弃元是一种在应用程序代码中人为取消使用的占位符变量。 弃元相当于未赋值的变量;它们没有值。 弃元将意图传达给编译器和其他读取代码的文件:你打算忽略表达式的结果。 你可能需要忽略表达式的结果、元组表达式的一个或多个成员、方法的 out 参数或模式匹配表达式的目标。

弃元使代码意图更加明确。 弃元指示代码永远不会使用变量。 它们可以增强其可读性和可维护性。

通过将下划线 (_) 赋给一个变量作为其变量名,指示该变量为一个占位符变量。 例如,以下方法调用返回一个元组,其中第一个值和第二个值为弃元。 area 是以前声明的变量,设置为由 GetCityInformation 返回的第三个组件:

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

可以使用弃元来指定 Lambda 表达式中未使用的输入参数。 有关详细信息,请参阅 Lambda 表达式一文中的 Lambda 表达式的输入参数一节。

_ 是有效弃元时,尝试检索其值或在赋值操作中使用它时,会生成编译器错误 CS0103:“当前上下文中不存在名称 ‘_’”。 出现此错误是因为 _ 未赋值,甚至可能未分配存储位置。 如果它是一个实际变量,则不能像之前的示例那样对多个值使用弃元。

元组和对象析构

如果应用程序代码使用某些元组元素,但忽略其他元素,这时使用弃元来处理元组就会很有用。 例如,以下 QueryCityDataForYears 方法返回一个元组,包含城市名称、城市面积、一个年份、该年份的城市人口、另一个年份及该年份的城市人口。 该示例显示了两个年份之间人口的变化。 对于元组提供的数据,我们不关注城市面积,并在一开始就知道城市名称和两个日期。 因此,我们只关注存储在元组中的两个人口数量值,可将其余值作为占位符处理。

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

有关使用占位符析构元组的详细信息,请参阅 析构元组和其他类型

类、结构或接口的 Deconstruct 方法还允许从对象中检索和析构一组特定的数据。 如果想只使用析构值的一个子集,可使用弃元。 以下示例将 Person 对象析构为四个字符串(名字、姓氏、城市和省/市/自治区),但舍弃姓氏和省/市/自治区。

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

有关使用弃元析构用户定义的类型的详细信息,请参阅 析构元组和其他类型

利用 switch 的模式匹配

弃元模式可通过 switch 表达式用于模式匹配。 每个表达式(包括 null)都始终匹配弃元模式。

以下示例定义了一个 ProvidesFormatInfo 方法,它使用 switch 表达式来确定对象是否提供 IFormatProvider 实现并测试对象是否为 null。 它还使用占位符模式来处理任何其他类型的非 null 对象。

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

对具有 out 参数的方法的调用

当调用 Deconstruct 方法来析构用户定义类型(类、结构或接口的实例)时,可使用占位符表示单个 out 参数的值。 但当使用 out 参数调用任何方法时,也可使用弃元表示 out 参数的值。

以下示例调用 DateTime.TryParse(String, out DateTime) 方法来确定日期的字符串表示形式在当前区域性中是否有效。 因为该示例侧重验证日期字符串,而不是解析它来提取日期,所以方法的 out 参数为占位符。

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

独立弃元

可使用独立弃元来指示要忽略的任何变量。 一种典型的用法是使用赋值来确保一个参数不为 null。 下面的代码使用弃元来强制赋值。 赋值的右侧使用 Null 合并操作符,用于在参数为 null 时引发 System.ArgumentNullException。 此代码不需要赋值结果,因此将对其使用弃元。 该表达式强制执行 null 检查。 弃元说明你的意图:不需要或不使用赋值结果。

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

    // Do work with arg.
}

以下示例使用独立占位符来忽略异步操作返回的 Task 对象。 分配任务的效果等同于抑制操作即将完成时所引发的异常。 这使你的意图更加明确:你需要对 Task 使用弃元,并忽略该异步操作生成的任何错误。

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

如果不将任务分配给弃元,则以下代码会生成编译器警告:

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

注意

如果使用调试器运行前面两个示例中的任意一个,则在引发异常时,调试器将停止该程序。 在没有附加调试器的情况下,这两种情况下的异常都会被以静默方式忽略。

_ 也是有效标识符。 当在支持的上下文之外使用时,_ 不视为占位符,而视为有效变量。 如果名为 _ 的标识符已在范围内,则使用 _ 作为独立占位符可能导致:

  • 将预期的占位符的值赋给范围内 _ 变量,会导致该变量的值被意外修改。 例如:
    private static void ShowValue(int _)
    {
       byte[] arr = [0, 0, 1, 2];
       _ = BitConverter.ToInt32(arr, 0);
       Console.WriteLine(_);
    }
     // The example displays the following output:
     //       33619968
    
  • 因违反类型安全而发生的编译器错误。 例如:
    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'
    
  • 编译器错误 CS0136:“无法在此范围中声明名为 "_" 的局部变量或参数,因为该名称用于在封闭的局部范围中定义局部变量或参数”。例如:
     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
    

另请参阅