C# 7.0 向 C# 语言添加了许多新功能html
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
变量的位置声明该变量,使得在分配它以前不可能意外使用它。低于 C# 7.0 的版本中也提供元组,但它们效率低下且不具备语言支持。 这意味着元组元素只能做为 Item1 和 Item2 等引用。 C# 7.0 引入了对元组的语言支持,可利用更有效的新元组类型向元组字段赋予语义名称。 c++
(string Alpha, string Beta) namedLetters = ("a", "b"); Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");
namedLetters
元组包含称为 Alpha
和 Beta
的字段。 这些名称仅存在于编译时且不保留,例如在运行时使用反射来检查元组时。git
在进行元组赋值时,还能够指定赋值右侧的字段的名称:程序员
var alphabetStart = (Alpha: "a", Beta: "b"); Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");
在某些时候,你可能想要解包从方法返回的元组的成员。 可经过为元组中的每一个值声明单独的变量来实现此目的。 这种解包操做称为解构元组 :github
(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
来提取各个字段:express
var p = new Point(3.14, 2.71);
(double X, double Y) = p;
可在元组相关文章中深刻了解有关元组的详细信息。编程
一般,在进行元组解构或使用 out
参数调用方法时,必须定义一个其值可有可无且你不打算使用的变量。 为处理此状况,C# 增添了对弃元的支持 。 弃元是一个名为 _
(下划线字符)的只写变量,可向单个变量赋予要放弃的全部值。 弃元相似于未赋值的变量;不可在代码中使用弃元(赋值语句除外)。api
在如下方案中支持弃元:数组
如下示例定义了 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."); } }
Name
属性的字符串为 null
,则将 throw
表达式与 null 合并运算符结合使用以引起异常。public string Name { get => name; set => name = value ?? throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null"); }
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;
综观来讲,你能够声明可读性更强的数值常量。