弃元 - 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 合并操作符,用于在参数为 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'
    

另请参阅