C# 7.0 向 C# 语言添加了许多新功能
out
参数的现有语法已在此版本中得到改进。 现在可以在方法调用的参数列表中声明 out
变量,而不是编写单独的声明语句:if (int.TryParse(input, out int result)) Console.WriteLine(result); else Console.WriteLine("Could not parse input");
out
变量的类型,如上所示。 但是,该语言支持使用隐式类型的局部变量:if (int.TryParse(input, out var answer)) Console.WriteLine(answer); else Console.WriteLine("Could not parse input");
- 代码更易于阅读。
- 在使用 out 变量的地方声明 out 变量,而不是在上面的另一行。
- 无需分配初始值。
- 通过在方法调用中使用
out
变量的位置声明该变量,使得在分配它之前不可能意外使用它。
- 通过在方法调用中使用
低于 C# 7.0 的版本中也提供元组,但它们效率低下且不具有语言支持。 这意味着元组元素只能作为 Item1 和 Item2 等引用。 C# 7.0 引入了对元组的语言支持,可利用更有效的新元组类型向元组字段赋予语义名称。
(string Alpha, string Beta) namedLetters = ("a", "b"); Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");
namedLetters
元组包含称为 Alpha
和 Beta
的字段。 这些名称仅存在于编译时且不保留,例如在运行时使用反射来检查元组时。
在进行元组赋值时,还可以指定赋值右侧的字段的名称:
var alphabetStart = (Alpha: "a", Beta: "b"); Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");
在某些时候,你可能想要解包从方法返回的元组的成员。 可通过为元组中的每个值声明单独的变量来实现此目的。 这种解包操作称为解构元组 :
(int max, int min) = Range(numbers); Console.WriteLine(max); Console.WriteLine(min);
还可以为 .NET 中的任何类型提供类似的析构。 编写 Deconstruct
方法,用作类的成员。Deconstruct
方法为你要提取的每个属性提供一组 out
参数。 考虑提供析构函数方法的此 Point
类,该方法提取 X
和 Y
坐标:
public class Point { public double X { get; } public double Y { get; } public Point(double x, double y) => (X, Y) = (x, y); public void Deconstruct(out double x, out double y) => (x, y) = (X, Y); }
可以通过向元组分配 Point
来提取各个字段:
var p = new Point(3.14, 2.71);
(double X, double Y) = p;
可在元组相关文章中深入了解有关元组的详细信息。
通常,在进行元组解构或使用 out
参数调用方法时,必须定义一个其值无关紧要且你不打算使用的变量。 为处理此情况,C# 增添了对弃元的支持 。 弃元是一个名为 _
(下划线字符)的只写变量,可向单个变量赋予要放弃的所有值。 弃元类似于未赋值的变量;不可在代码中使用弃元(赋值语句除外)。
在以下方案中支持弃元:
以下示例定义了 QueryCityDataForYears
方法,它返回一个包含两个不同年份的城市数据的六元组。 本例中,方法调用仅与此方法返回的两个人口值相关,因此在进行元组解构时,将元组中的其余值视为弃元。
1 using System; 2 using System.Collections.Generic; 3 4 public class Example 5 { 6 public static void Main() 7 { 8 var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010); 9 10 Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}"); 11 } 12 13 private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2) 14 { 15 int population1 = 0, population2 = 0; 16 double area = 0; 17 18 if (name == "New York City") { 19 area = 468.48; 20 if (year1 == 1960) { 21 population1 = 7781984; 22 } 23 if (year2 == 2010) { 24 population2 = 8175133; 25 } 26 return (name, area, year1, population1, year2, population2); 27 } 28 29 return ("", 0, 0, 0, 0, 0); 30 } 31 } 32 // 输出结果: 33 // Population change, 1960 to 2010: 393,149
有关详细信息,请参阅弃元。
模式匹配 是一种可让你对除对象类型以外的属性实现方法分派的功能。 你可能已经熟悉基于对象类型的方法分派。 在面向对象的编程中,虚拟和重写方法提供语言语法来实现基于对象类型的方法分派。 基类和派生类提供不同的实现。 模式匹配表达式扩展了这一概念,以便你可以通过继承层次结构为不相关的类型和数据元素轻松实现类似的分派模式。
模式匹配支持 is
表达式和 switch
表达式。 每个表达式都允许检查对象及其属性以确定该对象是否满足所寻求的模式。 使用 when
关键字来指定模式的其他规则。
is
模式表达式扩展了常用 is
运算符以查询关于其类型的对象,并在一条指令分配结果。以下代码检查变量是否为 int
,如果是,则将其添加到当前总和:
if (input is int count) sum += count;
前面的小型示例演示了 is
表达式的增强功能。 可以针对值类型和引用类型进行测试,并且可以将成功结果分配给类型正确的新变量。
switch 匹配表达式具有常见的语法,它基于已包含在 C# 语言中的 switch
语句。 更新后的 switch 语句有几个新构造:
switch
表达式的控制类型不再局限于整数类型、Enum
类型、string
或与这些类型之一对应的可为 null 的类型。 可能会使用任何类型。- 可以在每个
case
标签中测试switch
表达式的类型。 与is
表达式一样,可以为该类型指定一个新变量。 - 可以添加
when
子句以进一步测试该变量的条件。 case
标签的顺序现在很重要。 执行匹配的第一个分支;其他将跳过。
以下代码演示了这些新功能:
public static int SumPositiveNumbers(IEnumerable<object> sequence) { int sum = 0; foreach (var i in sequence) { switch (i) { case 0: break; case IEnumerable<int> childSequence: { foreach(var item in childSequence) sum += (item > 0) ? item : 0; break; } case int n when n > 0: sum += n; break; case null: throw new NullReferenceException("Null found in sequence"); default: throw new InvalidOperationException("Unrecognized type"); } } return sum; }
case 0:
是常见的常量模式。case IEnumerable<int> childSequence:
是一种类型模式。case int n when n > 0:
是具有附加when
条件的类型模式。case null:
是 null 模式。default:
是常见的默认事例。
可以在 C# 中的模式匹配中了解有关模式匹配的更多信息。
/// <summary> /// Ref局部变量和返回结果 /// </summary> public class MatrixSearch { public static ref int Find(int[,] matrix, Func<int, bool> predicate) { for (int i = 0; i < matrix.GetLength(0); i++) { for (int j = 0; j < matrix.GetLength(1); j++) { if (predicate(matrix[i, j])) { return ref matrix[i, j]; } } } throw new InvalidOperationException("Not found"); } }
可以将返回值声明为 ref
并在矩阵中修改该值,如以下代码所示:
int[,] matrix = new int[5,6]; ref var item = ref MatrixSearch.Find(matrix, (val) => val == 42); Console.WriteLine(item); item = 24; Console.WriteLine(matrix[4, 2]);
C# 语言还有多个规则,可保护你免于误用 ref
局部变量和返回结果:
- 必须将
ref
关键字添加到方法签名和方法中的所有return
语句中。- 这清楚地表明,该方法在整个方法中通过引用返回。
- 可以将
ref return
分配给值变量或ref
变量。- 调用方控制是否复制返回值。 在分配返回值时省略
ref
修饰符表示调用方需要该值的副本,而不是对存储的引用。
- 调用方控制是否复制返回值。 在分配返回值时省略
- 不可向
ref
本地变量赋予标准方法返回值。- 因为那将禁止类似
ref int i = sequence.Count();
这样的语句
- 因为那将禁止类似
- 不能将
ref
返回给其生存期不超出方法执行的变量。- 这意味着不可返回对本地变量或对类似作用域变量的引用。
ref
局部变量和返回结果不可用于异步方法。- 编译器无法知道异步方法返回时,引用的变量是否已设置为其最终值。
添加 ref 局部变量和 ref 返回结果可通过避免复制值或多次执行取消引用操作,允许更为高效的算法。
向返回值添加 ref
是源兼容的更改。 现有代码会进行编译,但在分配时复制 ref 返回值。调用方必须将存储的返回值更新为 ref
局部变量,从而将返回值存储为引用。
有关详细信息,请参阅 ref 关键字一文。
许多类的设计都包括仅从一个位置调用的方法。 这些额外的私有方法使每个方法保持小且集中。 本地函数使你能够在另一个方法的上下文内声明方法 。 本地函数使得类的阅读者更容易看到本地方法仅从声明它的上下文中调用。
对于本地函数有两个常见的用例:公共迭代器方法和公共异步方法。 这两种类型的方法都生成报告错误的时间晚于程序员期望时间的代码。 在迭代器方法中,只有在调用枚举返回的序列的代码时才会观察到任何异常。 在异步方法中,只有当返回的 Task
处于等待状态时才会观察到任何异常。 以下示例演示如何使用本地函数将参数验证与迭代器实现分离:
1 public static IEnumerable<char> AlphabetSubset3(char start, char end) 2 { 3 if (start < 'a' || start > 'z') 4 throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter"); 5 if (end < 'a' || end > 'z') 6 throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter"); 7 8 if (end <= start) 9 throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}"); 10 11 return AlphabetSubsetImplementation(); 12 13 IEnumerable<char> AlphabetSubsetImplementation() 14 { 15 for (var c = start; c < end; c++) 16 { 17 yield return c; 18 } 19 } 20 }
可以对 async
方法采用相同的技术,以确保在异步工作开始之前引发由参数验证引起的异常:
1 public Task<string> PerformLongRunningWork(string address, int index, string name) 2 { 3 if (string.IsNullOrWhiteSpace(address)) 4 throw new ArgumentException(message: "An address is required", paramName: nameof(address)); 5 if (index < 0) 6 throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative"); 7 if (string.IsNullOrWhiteSpace(name)) 8 throw new ArgumentException(message: "You must supply a name", paramName: nameof(name)); 9 10 return LongRunningWorkImplementation(); 11 12 async Task<string> LongRunningWorkImplementation() 13 { 14 var interimResult = await FirstWork(address); 15 var secondResult = await SecondStep(index, name); 16 return $"The results are {interimResult} and {secondResult}. Enjoy."; 17 } 18 } 19 20 private async Task<string> FirstWork(string address) 21 { 22 // await ··· 业务逻辑 23 return ""; 24 } 25 26 private async Task<string> SecondStep(int index, string name) 27 { 28 // await ··· 业务逻辑 29 return ""; 30 }
本地函数支持的某些设计也可以使用 lambda 表达式 来完成。 感兴趣的可以阅读有关差异的详细信息
get
和 set
访问器。 以下代码演示了每种情况的示例:public class ExpressionMembersExample { // Expression-bodied 构造函 public ExpressionMembersExample(string label) => this.Label = label; // Expression-bodied 终结器 ~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!"); private string label; // Expression-bodied get / set public string Label { get => label; set => this.label = value ?? "Default label"; } }
本示例不需要终结器,但显示它是为了演示语法。 不应在类中实现终结器,除非有必要发布非托管资源。 还应考虑使用 SafeHandle 类,而不是直接管理非托管资源。
这些 expression-bodied 成员的新位置代表了 C# 语言的一个重要里程碑:这些功能由致力于开发开放源代码 Roslyn 项目的社区成员实现。
将方法更改为 expression bodied 成员是二进制兼容的更改。
在 C# 中,throw
始终是一个语句。 因为 throw
是一个语句而非表达式,所以在某些 C# 构造中无法使用它。 它们包括条件表达式、null 合并表达式和一些 lambda 表达式。 添加 expression-bodied 成员将添加更多位置,在这些位置中,throw
表达式会很有用。 为了可以编写这些构造,C# 7.0 引入了 throw 表达式。这使得编写更多基于表达式的代码变得更容易。 不需要其他语句来进行错误检查。
从 C# 7.0 开始,throw
可以用作表达式和语句。 这允许在以前不支持的上下文中引发异常。 这些方法包括:
-
条件运算符。 下例使用
throw
表达式在向方法传递空字符串数组时引发 ArgumentException。 在 C# 7.0 之前,此逻辑将需要显示在if
/else
语句中。
private static void DisplayFirstNumber(string[] args) { string arg = args.Length >= 1 ? args[0] : throw new ArgumentException("You must supply an argument"); if (Int64.TryParse(arg, out var number)) { Console.WriteLine($"You entered {number:F0}"); } else { Console.WriteLine($"{arg} is not a number."); } }
- null 合并运算符。 在以下示例中,如果分配给
Name
属性的字符串为null
,则将throw
表达式与 null 合并运算符结合使用以引发异常。
public string Name { get => name; set => name = value ?? throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null"); }
- expression-bodied lambda 或方法。 下例说明了 expression-bodied 方法,由于不支持对 DateTime 值的转换,该方法引发 InvalidCastException。
DateTime ToDateTime(IFormatProvider provider) => throw new InvalidCastException("Conversion to a DateTime is not supported.");
从异步方法返回 Task
对象可能在某些路径中导致性能瓶颈。 Task
是引用类型,因此使用它意味着分配对象。 如果使用 async
修饰符声明的方法返回缓存结果或以同步方式完成,那么额外的分配在代码的性能关键部分可能要耗费相当长的时间。 如果这些分配发生在紧凑循环中,则成本会变高。
新语言功能意味着异步方法返回类型不限于 Task
、Task<T>
和 void
。 返回类型必须仍满足异步模式,这意味着 GetAwaiter
方法必须是可访问的。 作为一个具体示例,已将 ValueTask
类型添加到 .NET framework 中,以使用这一新语言功能:
public async ValueTask<int> Func() { await Task.Delay(100); return 5; }
需要添加 NuGet 包 System.Threading.Tasks.Extensions 才能使用 ValueTask 类型。
此增强功能对于库作者最有用,可避免在性能关键型代码中分配 Task
。
误读的数值常量可能使第一次阅读代码时更难理解。 位掩码或其他符号值容易产生误解。C# 7.0 包括两项新功能,可用于以最可读的方式写入数字来用于预期用途:二进制文本和数字分隔符 。
在创建位掩码时,或每当数字的二进制表示形式使代码最具可读性时,以二进制形式写入该数字:
public const int Sixteen = 0b0001_0000; public const int ThirtyTwo = 0b0010_0000; public const int SixtyFour = 0b0100_0000; public const int OneHundredTwentyEight = 0b1000_0000;
常量开头的 0b
表示该数字以二进制数形式写入。 二进制数可能会很长,因此通过引入 _
作为数字分隔符通常更易于查看位模式,如上面二进制常量所示。 数字分隔符可以出现在常量的任何位置。 对于十进制数字,通常将其用作千位分隔符:
public const long BillionsAndBillions = 100_000_000_000;
数字分隔符也可以与 decimal
、float
和 double
类型一起使用:
public const double AvogadroConstant = 6.022_140_857_747_474e23; public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;
综观来说,你可以声明可读性更强的数值常量。