弃元 - C# 指南Discards - C# Guide

从 C# 7.0 开始,C# 支持弃元,这是一种在应用程序代码中人为取消使用的临时虚拟变量。Starting with C# 7.0, C# supports discards, which are temporary, dummy variables that are intentionally unused in application code. 弃元相当于未赋值的变量;它们没有值。Discards are equivalent to unassigned variables; they do not have a value. 因为只有一个弃元变量,甚至不为该变量分配存储空间,所以弃元可减少内存分配。Because there is only a single discard variable, and that variable may not even be allocated storage, discards can reduce memory allocations. 因为它们使代码的意图清楚,增强了其可读性和可维护性。Because they make the intent of your code clear, they enhance its readability and maintainability.

通过将下划线 (_) 赋给一个变量作为其变量名,指示该变量为一个占位符变量。You indicate that a variable is a discard by assigning it the underscore (_) as its name. 例如,下面的方法调用返回 3 元组,其中的第一个和第二个值是弃元,area 是一个以前声明的变量,它将设置为 GetCityInformation 返回的相应的第三个部分:For example, the following method call returns a 3-tuple in which the first and second values are discards and area is a previously declared variable to be set to the corresponding third component returned by GetCityInformation:

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

在 C# 7.0 中,支持在以下上下文的分配中使用弃元:In C# 7.0, discards are supported in assignments in the following contexts:

  • 元组和对象析构Tuple and object deconstruction.
  • 使用 isswitch 的模式匹配。Pattern matching with is and switch.
  • 对具有 out 参数的方法的调用。Calls to methods with out parameters.
  • 当范围内没有 _ 时,独立的 _A standalone _ when no _ is in scope.

_ 是有效占位符时,尝试检索其值或在赋值操作中使用它时会生成编译器错误 CS0301:当前上下文中不存在名称 "_"。When _ is a valid discard, attempting to retrieve its value or use it in an assignment operation generates compiler error CS0301, "The name '_' does not exist in the current context". 这是因为 _ 未赋值,甚至可能未分配存储位置。This is because _ is not assigned a value, and may not even be assigned a storage location. 如果它是一个实际变量,则不能像之前的示例那样对多个值使用占位符。If it were an actual variable, you could not discard more than one value, as the previous example did.

元组和对象析构Tuple and object deconstruction

如果应用程序代码使用某些元组元素,但忽略其他元素,这时使用占位符来处理元组就会特别有用。Discards are particularly useful in working with tuples when your application code uses some tuple elements but ignores others. 例如,以下的 QueryCityDataForYears 方法返回一个 6 元组,包含城市名称、城市面积、一个年份、该年份的城市人口、另一个年份及该年份的城市人口。For example, the following QueryCityDataForYears method returns a 6-tuple with the name of a city, its area, a year, the city's population for that year, a second year, and the city's population for that second year. 该示例显示了两个年份之间人口的变化。The example shows the change in population between those two years. 对于元组提供的数据,我们不关注城市面积,并在一开始就知道城市名称和两个日期。Of the data available from the tuple, we're unconcerned with the city area, and we know the city name and the two dates at design-time. 因此,我们只关注存储在元组中的两个人口数量值,可将其余值作为占位符处理。As a result, we're only interested in the two population values stored in the tuple, and can handle its remaining values as discards.

using System;
using System.Collections.Generic;

public class Example
{
   public static void Main()
   {
       var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

       Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
   }
   
   private 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

有关使用占位符析构元组的详细信息,请参阅 析构元组和其他类型For more information on deconstructing tuples with discards, see Deconstructing tuples and other types.

类、结构或接口的 Deconstruct 方法还允许从对象中检索和析构一组特定的数据。The Deconstruct method of a class, structure, or interface also allows you to retrieve and deconstruct a specific set of data from an object. 如果想只使用析构值的一个子集时,可使用弃元。You can use discards when you are interested in working with only a subset of the deconstructed values. 以下示例将 Person 对象析构为四个字符串(名字、姓氏、城市和省/市/自治区),但舍弃姓氏和省/市/自治区。The following example deconstructs a Person object into four strings (the first and last names, the city, and the state), but discards the last name and the state.

using System;

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

public class Example
{
   public static void Main()
   {
       Person p = new Person("John", "Quincy", "Adams", "Boston", "MA");

       // <Snippet1>
       // Deconstruct the person object.
       var (fName, _, city, _) = p;
       Console.WriteLine($"Hello {fName} of {city}!");
       // The example displays the following output:
       //      Hello John of Boston!       
       // </Snippet1>
   }
}
// The example displays the following output:
//    Hello John Adams of Boston, MA!

有关使用弃元析构用户定义的类型的详细信息,请参阅 析构元组和其他类型For more information on deconstructing user-defined types with discards, see Deconstructing tuples and other types.

使用 switchis 的模式匹配Pattern matching with switch and is

弃元模式可通过 isswitch 关键字用于模式匹配。The discard pattern can be used in pattern matching with the is and switch keywords. 每个表达式始终匹配弃元模式。Every expression always matches the discard pattern.

以下示例定义了一个 ProvidesFormatInfo 方法,该方法使用 is 语句来确定对象是否提供 IFormatProvider 实现并测试对象是否为 nullThe following example defines a ProvidesFormatInfo method that uses is statements to determine whether an object provides an IFormatProvider implementation and tests whether the object is null. 它还使用占位符模式来处理任何其他类型的非 null 对象。It also uses the discard pattern to handle non-null objects of any other type.

using System;
using System.Globalization;

public class Example
{
   public static void Main()
   {
      object[] objects = { CultureInfo.CurrentCulture, 
                           CultureInfo.CurrentCulture.DateTimeFormat, 
                           CultureInfo.CurrentCulture.NumberFormat,
                           new ArgumentException(), null };
      foreach (var obj in objects)
         ProvidesFormatInfo(obj);
   }

   private static void ProvidesFormatInfo(object obj)         
   {
      switch (obj)
      {
         case IFormatProvider fmt:
            Console.WriteLine($"{fmt} object");
            break;
         case null:
            Console.Write("A null object reference: ");
            Console.WriteLine("Its use could result in a NullReferenceException");
            break;
         case object _:
            Console.WriteLine("Some object type without format information");
            break;
      }
   }
}
// The example displays the following output:
//    en-US 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 参数调用方法Calls to methods with out parameters

当调用 Deconstruct 方法来析构用户定义类型(类、结构或接口的实例)时,可使用占位符表示单个 out 参数的值。When calling the Deconstruct method to deconstruct a user-defined type (an instance of a class, structure, or interface), you can discard the values of individual out arguments. 但当使用 out 参数调用任何方法时,也可使用占位符表示 out 参数的值。But you can also discard the value of out arguments when calling any method with an out parameter.

以下示例调用 DateTime.TryParse(String, out DateTime) 方法来确定日期的字符串表示形式在当前区域性中是否有效。The following example calls the DateTime.TryParse(String, out DateTime) method to determine whether the string representation of a date is valid in the current culture. 因为该示例侧重验证日期字符串,而不是解析它来提取日期,所以方法的 out 参数为占位符。Because the example is concerned only with validating the date string and not with parsing it to extract the date, the out argument to the method is a discard.

using System;

public class Example
{
   public static void Main()
   {
      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

独立弃元A standalone discard

可使用独立弃元来指示要忽略的任何变量。You can use a standalone discard to indicate any variable that you choose to ignore. 以下示例使用独立占位符来忽略异步操作返回的 Task 对象。The following example uses a standalone discard to ignore the Task object returned by an asynchronous operation. 这一操作的效果等同于抑制操作即将完成时所引发的异常。This has the effect of suppressing the exception that the operation throws as it is about to complete.

using System;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
      ExecuteAsyncMethods().Wait();
   }

   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

请注意,_ 也是有效标识符。Note that _ is also a valid identifier. 当在支持的上下文之外使用时,_ 不视为占位符,而视为有效变量。When used outside of a supported context, _ is treated not as a discard but as a valid variable. 如果名为 _ 的标识符已在范围内,则使用 _ 作为独立占位符可能导致:If an identifier named _ is already in scope, the use of _ as a standalone discard can result in:

  • 将预期的占位符的值赋给范围内 _ 变量,会导致该变量的值被意外修改。Accidental modification of the value of the in-scope _ variable by assigning it the value of the intended discard. 例如:For example:

    private static void ShowValue(int _)
    {
       byte[] arr = { 0, 0, 1, 2 };
       _ = BitConverter.ToInt32(arr, 0);
       Console.WriteLine(_);
    }
    // The example displays the following output:
    //       33619968
    
  • 因违反类型安全而发生的编译器错误。A compiler error for violating type safety. 例如:For example:

    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:“无法在此范围中声明名为“_”的局部变量或参数,因为该名称用于在封闭的局部范围中定义局部变量或参数”。Compiler 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." 例如:For example:

    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   
    

请参阅See also