C# 7.0 中的新增功能

out 变量

可以将 out 值内联作为参数声明到使用这些参数的方法中。

无需分配初始值

1
2
3
4
if (int.TryParse(input, out var answer))
Console.WriteLine(answer);
else
Console.WriteLine("Could not parse input");

元组

低于 C# 7.0 的版本中也提供元组,但它们效率低下且不具有语言支持。 这意味着元组元素只能作为 Item1 和 Item2 等引用。 C# 7.0 引入了对元组的语言支持,可利用更有效的新元组类型向元组字段赋予语义名称。这些名称仅存在于编译时且不保留,例如在运行时使用反射来检查元组时。

1
2
3
4
5
6
7
8
9
10
11
// 第一种写法
(string Alpha, string Beta) namedLetters = ("a", "b");

// 第二种写法
var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");

// 第三种写法
(int max, int min) = Range(numbers);
Console.WriteLine(max);
Console.WriteLine(min);

从 C# 7.3 开始,元组类型支持 == 和 != 运算符。这些运算符按照元组元素的顺序将左侧操作数的成员与相应的右侧操作数的成员进行比较。

参考:

元组类型(C# 参考)

弃元

弃元是一个名为 _(下划线字符)的只写变量,可向单个变量赋予要放弃的所有值。 弃元类似于未赋值的变量;不可在代码中使用弃元(赋值语句除外)。

在以下方案中支持弃元:

  • 在对元组或用户定义的类型进行解构时。
  • 在使用 out 参数调用方法时。
  • 在使用 is 和 switch 语句匹配操作的模式中。
  • 在要将某赋值的值显式标识为弃元时用作独立标识符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

模式匹配

模式匹配支持 is 表达式和 switch 表达式。 每个表达式都允许检查对象及其属性以确定该对象是否满足所寻求的模式。 使用 when 关键字来指定模式的其他规则。

1
2
if (input is int count)
sum += count;

更新后的 switch 语句有几个新构造:

  • switch 表达式的控制类型不再局限于整数类型、Enum 类型、string 或与这些类型之一对应的可为 null 的类型。 可能会使用任何类型。
  • 可以在每个 case 标签中测试 switch 表达式的类型。 与 is 表达式一样,可以为该类型指定一个新变量。
  • 可以添加 when 子句以进一步测试该变量的条件。
  • case 标签的顺序现在很重要。 执行匹配的第一个分支;其他将跳过。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 childSequence: 是一种类型模式。
  • case int n when n > 0: 是具有附加 when 条件的类型模式。
  • case null: 是 null 模式。
  • default: 是常见的默认事例。

Ref 局部变量和返回结果

此功能允许使用并返回对变量的引用的算法,这些变量在其他位置定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 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 返回结果可通过避免复制值或多次执行取消引用的操作,允许更为高效的算法。

本地函数

方法套方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Task<string> PerformLongRunningWork(string address, int index, string name)
{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException(message: "An address is required", paramName: nameof(address));
if (index < 0)
throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

return longRunningWorkImplementation();

async Task<string> longRunningWorkImplementation()
{
var interimResult = await FirstWork(address);
var secondResult = await SecondStep(index, name);
return $"The results are {interimResult} and {secondResult}. Enjoy.";
}
}

更多的 expression-bodied 成员

C# 7.0 扩展了可作为表达式实现的允许的成员。 在 C# 7.0 中,你可以在属性和索引器上实现构造函数、终结器以及 get 和 set 访问器。 以下代码演示了每种情况的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// Expression-bodied get / set accessors.
public string Label
{
get => label;
set => this.label = value ?? "Default label";
}

本示例不需要终结器,但显示它是为了演示语法。 不应在类中实现终结器,除非有必要发布非托管资源。 还应考虑使用 SafeHandle 类,而不是直接管理非托管资源。

throw 表达式

  • 条件运算符。 下例使用 throw 表达式在向方法传递空字符串数组时引发 ArgumentException。 在 C# 7.0 之前,此逻辑将需要显示在 if/else 语句中。
1
2
3
4
5
6
7
8
9
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 合并运算符结合使用以引发异常。
1
2
3
4
5
6
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。
1
2
DateTime ToDateTime(IFormatProvider provider) =>
throw new InvalidCastException("Conversion to a DateTime is not supported.");

通用的异步返回类型

添加 NuGet 包 System.Threading.Tasks.Extensions 才能使用 ValueTask 类型。

新语言功能意味着异步方法返回类型不限于 Task、Task 和 void。

1
2
3
4
5
public async ValueTask<int> Func()
{
await Task.Delay(100);
return 5;
}

数字文本语法改进

C# 7.0 包括两项新功能,可用于以最可读的方式写入数字来用于预期用途:二进制文本和数字分隔符 。
在创建位掩码时,或每当数字的二进制表示形式使代码最具可读性时,以二进制形式写入该数字:

1
2
3
4
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 表示该数字以二进制数形式写入。 二进制数可能会很长,因此通过引入 _ 作为数字分隔符通常更易于查看位模式,如上面二进制常量所示。

数字分隔符可以出现在常量的任何位置。 对于十进制数字,通常将其用作千位分隔符:

1
2
3
4
5
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;

综观来说,你可以声明可读性更强的数值常量。

参考:

C# 7.0 中的新增功能