C# 7.0 - C# 7.3 为 C# 开发体验带来了大量功能和增量改进。 本文概述了新的语言功能和编译器选项。 说明中描述了 C# 7.3 的行为,C# 7.3 是基于 .NET Framework 的应用程序支持的最新版本。
C# 7.1 中添加了语言版本选择配置元素,因此你可以在项目文件中指定编译器语言版本。
C# 7.0-7.3 将这些功能和主题添加到了 C# 语言中:
out
参数调用方法时,它们的作用最大。async
Main
方法
async
修饰符。throw
表达式
throw
是语句而不被允许的代码构造中引发异常。default
文本表达式
out
变量
out
值内联作为参数声明到使用这些参数的方法中。private protected
访问修饰符
private protected
访问修饰符允许访问同一程序集中的派生类。最后,编译器中添加了新的选项:
-refout
和 -refonly
。-publicsign
,用于启用程序集的开放源代码软件 (OSS) 签名。-pathmap
用于提供源目录的映射。本文的其余部分概述了每个功能。 你将了解每项功能背后的原理和语法。 可以使用 dotnet try
全局工具在环境中浏览这些功能:
dotnet try
。C# 为用于说明设计意图的类和结构提供了丰富的语法。 但是,这种丰富的语法有时会需要额外的工作,但益处却很少。 你可能经常编写需要包含多个数据元素的简单结构的方法。 为了支持这些方案,已将元组添加到了 C#。 元组是包含多个字段以表示数据成员的轻量级数据结构。 这些字段没有经过验证,并且你无法定义自己的方法。 C# 元组类型支持 ==
和 !=
。 有关详细信息,
备注
低于 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
的字段。 这些名称仅存在于编译时且不保留,例如在运行时使用反射来检查元组时。
在进行元组赋值时,还可以指定赋值右侧的字段的名称:
C#
var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{alphabetStart.Alpha}, {alphabetStart.Beta}");
在某些时候,你可能想要解包从方法返回的元组的成员。 可通过为元组中的每个值声明单独的变量来实现此目的。 这种解包操作称为解构元组:
C#
(int max, int min) = Range(numbers);
Console.WriteLine(max);
Console.WriteLine(min);
还可以为 .NET 中的任何类型提供类似的析构。 编写 Deconstruct
方法,用作类的成员。 Deconstruct
方法为你要提取的每个属性提供一组 out
参数。 考虑提供析构函数方法的此 Point
类,该方法提取 X
和 Y
坐标:
C#
public class Point
{
public Point(double x, double y)
=> (X, Y) = (x, y);
public double X { get; }
public double Y { get; }
public void Deconstruct(out double x, out double y) =>
(x, y) = (X, Y);
}
可以通过向元组分配 Point
来提取各个字段:
C#
var p = new Point(3.14, 2.71);
(double X, double Y) = p;
在初始化元组时,许多时候,赋值操作右侧的变量名与用于元组元素的名称相同:元组元素的名称可通过用于初始化元组的变量进行推断:
C#
int count = 5;
string label = "Colors used in the map";
var pair = (count, label); // element names are "count" and "label"
若要详细了解此功能,可以参阅元组类型一文。
通常,在进行元组解构或使用 out
参数调用方法时,必须定义一个其值无关紧要且你不打算使用的变量。 为处理此情况,C# 增添了对弃元的支持。 弃元是一个名为 _
(下划线字符)的只写变量,可向单个变量赋予要放弃的所有值。 弃元类似于未赋值的变量;不可在代码中使用弃元(赋值语句除外)。
在以下方案中支持弃元:
以下示例定义了 QueryCityDataForYears
方法,它返回一个包含两个不同年份的城市数据的六元组。 本例中,方法调用仅与此方法返回的两个人口值相关,因此在进行元组解构时,将元组中的其余值视为弃元。
C#
using System;
public class Example
{
public static void Main()
{
var result = QueryCityData("New York City");
var city = result.Item1;
var pop = result.Item2;
var size = result.Item3;
// Do something with the data.
}
private static (string, int, double) QueryCityData(string name)
{
if (name == "New York City")
return (name, 8175133, 468.48);
return ("", 0, 0);
}
}
有关详细信息,请参阅弃元。
模式匹配是一组功能,利用这些功能,你可以通过新的方式在代码中表示控制流。 你可以测试变量的类型、值或其属性的值。 这些方法可创建可读性更佳的代码流。
模式匹配支持 is
表达式和 switch
表达式。 每个表达式都允许检查对象及其属性以确定该对象是否满足所寻求的模式。 使用 when
关键字来指定模式的其他规则。
is
模式表达式扩展了常用 is
运算符以查询关于其类型的对象,并在一条指令分配结果。 以下代码检查变量是否为 int
,如果是,则将其添加到当前总和:
C#
if (input is int count)
sum += count;
前面的小型示例演示了 is
表达式的增强功能。 可以针对值类型和引用类型进行测试,并且可以将成功结果分配给类型正确的新变量。
switch 匹配表达式具有常见的语法,它基于已包含在 C# 语言中的 switch
语句。 更新后的 switch 语句有几个新构造:
switch
表达式的控制类型不再局限于整数类型、Enum
类型、string
或与这些类型之一对应的可为 null 的类型。 可能会使用任何类型。case
标签中测试 switch
表达式的类型。 与 is
表达式一样,可以为该类型指定一个新变量。when
子句以进一步测试该变量的条件。case
标签的顺序现在很重要。 执行匹配的第一个分支;其他将跳过。以下代码演示了这些新功能:
C#
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:
是常见的默认事例。自 C# 7.1 起,is
和 switch
类型模式的模式表达式的类型可能为泛型类型参数。 这可能在检查 struct
或 class
类型且要避免装箱时最有用。
可以在 C# 中的模式匹配中了解有关模式匹配的更多信息。
main
方法异步 Main 方法使你能够在 Main
方法中使用 await
关键字。 在过去,需要编写:
C#
static int Main()
{
return DoAsyncWork().GetAwaiter().GetResult();
}
现在,您可以编写:
C#
static async Task<int> Main()
{
// This could also be replaced with the body
// DoAsyncWork, including its await expressions:
return await DoAsyncWork();
}
如果程序不返回退出代码,可以声明返回 Task 的 Main
方法:
C#
static async Task Main()
{
await SomeAsyncMethod();
}
如需了解更多详情,可以阅读编程指南中的异步 Main 一文。
许多类的设计都包括仅从一个位置调用的方法。 这些额外的私有方法使每个方法保持小且集中。 本地函数使你能够在另一个方法的上下文内声明方法。 本地函数使得类的阅读者更容易看到本地方法仅从声明它的上下文中调用。
对于本地函数有两个常见的用例:公共迭代器方法和公共异步方法。 这两种类型的方法都生成报告错误的时间晚于程序员期望时间的代码。 在迭代器方法中,只有在调用枚举返回的序列的代码时才会观察到任何异常。 在异步方法中,只有当返回的 Task
处于等待状态时才会观察到任何异常。 以下示例演示如何使用本地函数将参数验证与迭代器实现分离:
C#
public static IEnumerable<char> AlphabetSubset3(char start, char end)
{
if (start < 'a' || start > 'z')
throw new ArgumentOutOfRangeException(paramName: nameof(start), message: "start must be a letter");
if (end < 'a' || end > 'z')
throw new ArgumentOutOfRangeException(paramName: nameof(end), message: "end must be a letter");
if (end <= start)
throw new ArgumentException($"{nameof(end)} must be greater than {nameof(start)}");
return alphabetSubsetImplementation();
IEnumerable<char> alphabetSubsetImplementation()
{
for (var c = start; c < end; c++)
yield return c;
}
}
可以对 async
方法采用相同的技术,以确保在异步工作开始之前引发由参数验证引起的异常:
C#
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.";
}
}
现在支持此语法:
C#
[field: SomeThingAboutFieldAttribute]
public int SomeProperty { get; set; }
属性 SomeThingAboutFieldAttribute
应用于编译器生成的 SomeProperty
的支持字段。 有关详细信息,请参阅 C# 编程指南中的属性。
备注
本地函数支持的某些设计也可以使用 lambda 表达式来完成。 有关详细信息,请参阅本地函数与 Lambda 表达式。
C# 6 为成员函数和只读属性引入了 expression-bodied 成员。 C# 7.0 扩展了可作为表达式实现的允许的成员。 在 C# 7.0 中,你可以在属性和索引器上实现构造函数、终结器以及 get
和 set
访问器。 以下代码演示了每种情况的示例:
C#
// 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 类,而不是直接管理非托管资源。
这些 expression-bodied 成员的新位置代表了 C# 语言的一个重要里程碑:这些功能由致力于开发开放源代码 Roslyn 项目的社区成员实现。
将方法更改为 expression bodied 成员是二进制兼容的更改。
在 C# 中,throw
始终是一个语句。 因为 throw
是一个语句而非表达式,所以在某些 C# 构造中无法使用它。 它们包括条件表达式、null 合并表达式和一些 lambda 表达式。 添加 expression-bodied 成员将添加更多位置,在这些位置中,throw
表达式会很有用。 为了可以编写这些构造,C# 7.0 引入了 throw 表达式。
这使得编写更多基于表达式的代码变得更容易。 不需要其他语句来进行错误检查。
默认文本表达式是针对默认值表达式的一项增强功能。 这些表达式将变量初始化为默认值。 过去会这么编写:
C#
Func<string, bool> whereClause = default(Func<string, bool>);
现在,可以省略掉初始化右侧的类型:
C#
Func<string, bool> whereClause = default;
有关详细信息,请参阅 default 运算符一文中的 default 文本部分。
误读的数值常量可能使第一次阅读代码时更难理解。 位掩码或其他符号值容易产生误解。 C# 7.0 包括两项新功能,可用于以最可读的方式写入数字来用于预期用途:二进制文本和数字分隔符 。
在创建位掩码时,或每当数字的二进制表示形式使代码最具可读性时,以二进制形式写入该数字:
C#
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
表示该数字以二进制数形式写入。 二进制数可能会很长,因此通过引入 _
作为数字分隔符通常更易于查看位模式,如前面示例中的二进制常量所示。 数字分隔符可以出现在常量的任何位置。 对于十进制数字,通常将其用作千位分隔符。 十六进制和二进制文本可采用 _
开头:
C#
public const long BillionsAndBillions = 100_000_000_000;
数字分隔符也可以与 decimal
、float
和 double
类型一起使用:
C#
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;
综观来说,你可以声明可读性更强的数值常量。
out
变量支持 out
参数的现有语法已在 C# 7 中得到改进。 现在可以在方法调用的参数列表中声明 out
变量,而不是编写单独的声明语句:
C#
if (int.TryParse(input, out int result))
Console.WriteLine(result);
else
Console.WriteLine("Could not parse input");
为清楚起见,我们建议你指定 out
变量的类型,如前面的示例所示。 但是,该语言支持使用隐式类型的局部变量:
C#
if (int.TryParse(input, out var answer))
Console.WriteLine(answer);
else
Console.WriteLine("Could not parse input");
out
变量的位置声明该变量,使得在分配它之前不可能意外使用它。已对在 C# 7.0 中添加的允许 out
变量声明的语法进行了扩展,以包含字段初始值设定项、属性初始值设定项、构造函数初始值设定项和查询子句。 它允许使用如以下示例中所示的代码:
C#
public class B
{
public B(int i, out int j)
{
j = i;
}
}
public class D : B
{
public D(int i) : base(i, out var j)
{
Console.WriteLine($"The value of 'j' is {j}");
}
}
方法调用现可使用位于位置参数前面的命名参数(若这些命名参数的位置正确)。 如需了解详情,请参阅命名参数和可选参数。
新的复合访问修饰符:private protected
指示可通过包含同一程序集中声明的类或派生类来访问成员。 虽然 protected internal
允许通过同一程序集中的类或派生类进行访问,但 private protected
限制对同一程序集中声明的派生类的访问。
如需了解详情,请参阅语言参考中的访问修饰符。
在每个版本中,对重载解析规则进行了更新,以解决多义方法调用具有“明显”选择的情况。 此版本添加了三个新规则,以帮助编译器选取明显的选择:
this
实例接收器无法使用)包含未定义 this
的成员的正文(例如,静态成员),以及不能使用 this
的位置(例如,字段初始值设定项和构造函数初始值设定项)。你将注意到此更改,因为当你确定哪个方法更好时,你将发现多义方法重载具有更少的编译器错误。
你应能够安全地编写性能与不安全代码一样好的 C# 代码。 安全代码可避免错误类,例如缓冲区溢出、杂散指针和其他内存访问错误。 这些新功能扩展了可验证安全代码的功能。 努力使用安全结构编写更多代码。 这些功能使其更容易实现。
以下新增功能支持使安全代码获得更好的性能的主题:
ref
本地变量。stackalloc
数组上的初始值设定项。fixed
语句。in
修饰符,指定形参通过引用传递,但不通过调用方法修改。 将 in
修饰符添加到参数是源兼容的更改。ref readonly
修饰符,指示方法通过引用返回其值,但不允许写入该对象。 如果向某个值赋予返回值,则添加 ref readonly
修饰符是源兼容的更改。 将 readonly
修饰符添加到现有的 ref
返回语句是不兼容的更改。 它要求调用方更新 ref
本地变量的声明以包含 readonly
修饰符。readonly struct
声明,指示结构不可变,且应作为 in
参数传递到其成员方法。 将 readonly
修饰符添加到现有的结构声明是二进制兼容的更改。ref struct
声明,指示结构类型直接访问托管的内存,且必须始终分配有堆栈。 将 ref
修饰符添加到现有 struct
声明是不兼容的更改。 ref struct
不能是类的成员,也不能用于可能在堆上分配的其他位置。可以在编写安全高效的代码中详细了解所有这些更改。
此功能允许使用并返回对变量的引用的算法,这些变量在其他位置定义。 一个示例是使用大型矩阵并查找具有某些特征的单个位置。 下面的方法在矩阵中向该存储返回“引用”:
C#
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
并在矩阵中修改该值,如以下代码所示:
C#
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
局部变量进行初始化后,可能会对其重新分配,以引用不同的实例。 以下代码现在编译:
C#
ref VeryLargeStruct refLocal = ref veryLargeStruct; // initialization
refLocal = ref anotherVeryLargeStruct; // reassigned, refLocal refers to different storage.
有关详细信息,请参阅有关 ref
返回和 ref
局部变量以及 foreach
的文章。
有关详细信息,请参阅 ref 关键字一文。
ref
表达式最后,条件表达式可能生成 ref 结果而不是值。 例如,你将编写以下内容以检索对两个数组之一中第一个元素的引用:
C#
ref var r = ref (arr != null ? ref arr[0] : ref otherArr[0]);
变量 r
是对 arr
或 otherArr
中第一个值的引用。
有关详细信息,请参阅语言参考中的条件运算符 (?。
in
参数修饰符in
关键字补充了现有的 ref 和 out 关键字,以按引用传递参数。 in
关键字指定按引用传递参数,但调用的方法不修改值。
你可以声明按值或只读引用传递的重载,如以下代码所示:
C#
static void M(S arg);
static void M(in S arg);
按值(前面示例中的第一个)传递的重载比按只读引用传递的重载更好。 若要使用只读引用参数调用版本,必须在调用方法前添加 in
修饰符。
有关详细信息,请参阅有关 in
参数修饰符的文章。
fixed
语句fixed
语句支持有限的一组类型。 从 C# 7.3 开始,任何包含返回 ref T
或 ref readonly T
的 GetPinnableReference()
方法的类型均有可能为 fixed
。 添加此功能意味着 fixed
可与 System.Span 和相关类型配合使用。
有关详细信息,请参阅语言参考中的 fixed
语句一文。
fixed
字段不需要进行固定请考虑此结构:
C#
unsafe struct S
{
public fixed int myFixedField[10];
}
在早期版本的 C# 中,需要固定变量才能访问属于 myFixedField
的整数之一。 现在,以下代码进行编译,而不将变量 p
固定到单独的 fixed
语句中:
C#
class C
{
static S s = new S();
unsafe public void M()
{
int p = s.myFixedField[5];
}
}
变量 p
访问 myFixedField
中的一个元素。 无需声明单独的 int*
变量。 仍需要 unsafe
上下文。 在早期版本的 C# 中,需要声明第二个固定的指针:
C#
class C
{
static S s = new S();
unsafe public void M()
{
fixed (int* ptr = s.myFixedField)
{
int p = ptr[5];
}
}
}
有关详细信息,请参阅有关 fixed
语句的文章。
stackalloc
数组支持初始值设定项当你对数组中的元素的值进行初始值设定时,你已能够指定该值:
C#
var arr = new int[3] {1, 2, 3};
var arr2 = new int[] {1, 2, 3};
现在,可向使用 stackalloc
进行声明的数组应用同一语法:
C#
int* pArr = stackalloc int[3] {1, 2, 3};
int* pArr2 = stackalloc int[] {1, 2, 3};
Span<int> arr = stackalloc [] {1, 2, 3};
有关详细信息,请参阅stackalloc
运算符一文。
现在,可以将类型 System.Enum 或 System.Delegate 指定为类型参数的基类约束。
现在也可以使用新的 unmanaged
约束来指定类型参数必须是不可为 null 的“非托管类型”。
有关详细信息,请参阅有关 where
泛型约束和类型参数的约束的文章。
将这些约束添加到现有类型是不兼容的更改。 封闭式泛型类型可能不再满足这些新约束的要求。
从异步方法返回 Task
对象可能在某些路径中导致性能瓶颈。 Task
是引用类型,因此使用它意味着分配对象。 如果使用 async
修饰符声明的方法返回缓存结果或以同步方式完成,那么额外的分配在代码的性能关键部分可能要耗费相当长的时间。 如果这些分配发生在紧凑循环中,则成本会变高。
新语言功能意味着异步方法返回类型不限于 Task
、Task
和 void
。 返回类型必须仍满足异步模式,这意味着 GetAwaiter
方法必须是可访问的。 作为一个具体示例,已将 ValueTask
类型添加到 .NET 中,以使用这一新语言功能:
C#
public async ValueTask<int> Func()
{
await Task.Delay(100);
return 5;
}
备注
你需要添加 NuGet 包 System.Threading.Tasks.Extensions
才能使用 ValueTask 类型。
此增强功能对于库作者最有用,可避免在性能关键型代码中分配 Task
。
新的编译器选项支持 C# 程序的新版本和 DevOps 方案。
有两种新的编译器选项可生成仅引用程序集:ProduceReferenceAssembly 和 ProduceOnlyReferenceAssembly。 链接的文章详细介绍了这些选项和引用程序集。
PublicSign 编译器选项指示编译器使用公钥对程序集进行签名。 程序集被标记为已签名,但签名取自公钥。 此选项使你能够使用公钥在开放源代码项目中构建签名的程序集。
有关详细信息,请参阅 PublicSign 编译器选项一文。
PathMap 编译器选项指示编译器将生成环境中的源路径替换为映射的源路径。 PathMap 选项控制由编译器编写入 PDB 文件或为 CallerFilePathAttribute 编写的源路径。
有关详细信息,请参阅 PathMap 编译器选项一文。