C# 7.2 中的新增功能
官方文档见 这里。本文在其基础上添加了一些自己理解及示例代码,如有不正确的地方欢迎指正。
C# 7.2 又是一个单点版本,它增添了大量有用的功能。
此版本的一项主要功能是避免不必要的复制或分配,进而更有效地处理值类型。
其余功能很微小,但值得拥有。
语言版本选择配置
C# 7.2 使用 语言版本选择 配置元素来选择编译器语言版本。
在 VS 2017 中你可以通过 项目属性 => 生成 => 高级 => 语言版本 来设置项目使用的 C# 版本。
但在 VS 2019 中创建的 .NET Core 3.0 项目中你会发现该选项已变灰且显示为 已根据框架版本自动选择 。点击 为何无法选择其它 C# 版本? 会跳转到 MSDN。
这里可以看到 目标框架各个版本 对应的 C# 语言版本的默认值。
目标框架 | version | C# 语言版本的默认值 |
---|---|---|
.NET Core | 3.x | C# 8.0 |
.NET Core | 2.x | C# 7.3 |
.NET Standard | 2.1 | C# 8.0 |
.NET Standard | 2.0 | C# 7.3 |
.NET Standard | 1.x | C# 7.3 |
.NET Framework | 全部 | C# 7.3 |
其中 .NET Core 3.0 对应的默认值就是 C# 8.0 。
如果必须修改项目的语言版本,需要手动修改项目文件。
修改方法:添加如下元素到项目文件(详情见 这里)。
<PropertyGroup>
<LangVersion>preview</LangVersion>
</PropertyGroup>
其中语言版本的所有值见下表。
值 | 含义 |
---|---|
preview | 编译器接受最新预览版中的所有有效语言语法。 |
最新 | 编译器接受最新发布的编译器版本(包括次要版本)中的语法。 |
latestMajor | 编译器接受最新发布的编译器主要版本中的语法。 |
8.0 | 编译器只接受 C# 8.0 或更低版本中所含的语法。 |
7.3 | 编译器只接受 C# 7.3 或更低版本中所含的语法。 |
7.2 | 编译器只接受 C# 7.2 或更低版本中所含的语法。 |
7.1 | 编译器只接受 C# 7.1 或更低版本中所含的语法。 |
7 | 编译器只接受 C# 7.0 或更低版本中所含的语法。 |
6 | 编译器只接受 C# 6.0 或更低版本中所含的语法。 |
5 | 编译器只接受 C# 5.0 或更低版本中所含的语法。 |
4 | 编译器只接受 C# 4.0 或更低版本中所含的语法。 |
3 | 编译器只接受 C# 3.0 或更低版本中所含的语法。 |
ISO-2 | 编译器只接受 ISO/IEC 23270:2006 C# (2.0) 中所含的语法 |
ISO-1 | 编译器只接受 ISO/IEC 23270:2003 C# (1.0/1.2) 中所含的语法 |
安全高效的代码的增强功能
in
& ref readonly
**针对实参的 in
修饰符,指定形参通过引用传递,但不通过调用方法修改。**示例如下:
public static Point3D Translate(in Point3D source, double dX, double dY, double dZ) =>
new Point3D(source.X + dX, source.Y + dY, source.Z + dZ);
其中 Point3D 是个 struct ,通过在形参中指定 in
修饰符,形参 source 是按地址传递的,这样可以减少值类型不必要的复制。
限制是形参 source 不能被修改,尝试修改会报错。
public static void Translate(in Point3D source, double dX, double dY, double dZ) =>
source = new Point3D(source.X + dX, source.Y + dY, source.Z + dZ);
上例会报错: 无法分配到 变量 'in InReadonly.Point3D' ,因为它是只读变量
调用时同样的需要对实参指定 in
修饰符。
start = Point3D.Translate(in start, 5, 5, 5);
针对方法返回的 ref readonly
修饰符,指示方法通过引用返回其值,但不允许写入该对象。
使用方法是在方法定义的 ref
后添加 readonly
修饰符。
public static ref readonly Point3D Origin => ref origin;
同样调用的地方也需要在 ref
后添加 readonly
修饰符。
ref readonly var start = ref Point3D.Origin;
若缺少 readonly
修饰符则会报错: 不能将 属性 'InReadonly.Point3D.Origin' 作为 ref 或 out 值使用,因为它是只读变量 。
readonly struct
readonly struct
声明,指示结构不可变,且应作为 in
参数传递到其成员方法。
使用 readonly
修饰符声明 struct
将通知编译器你的意图是创建不可变类型。编译器使用以下规则强制执行该设计决策:
- 所有字段成员必须为
readonly
- 所有属性都必须是只读的,包括自动实现的属性。
官方提供的示例结构 Point3D 代码如下。Point3D 被设计为 不可变的,但是编译器无法识别开发者的意图。这会导致在访问 Point3D 的只读成员的属性时,编译器会创建一个 防御副本(defensive copy) (这个可以在 IL 代码中看到,后面会有介绍)。
public struct Point3D
{
private static Point3D origin = new Point3D(0, 0, 0);
public static ref readonly Point3D Origin => ref origin;
public double X { get; }
public double Y { get; }
public double Z { get; }
private double? distance;
public Point3D(double x, double y, double z)
{
X = x;
Y = y;
Z = z;
distance = null;
}
public double ComputeDistance()
{
if (!distance.HasValue)
distance = Math.Sqrt(X * X + Y * Y + Z * Z);
return distance.Value;
}
public static Point3D Translate(in Point3D source, double dX, double dY, double dZ) =>
new Point3D(source.X + dX, source.Y + dY, source.Z + dZ);
public override string ToString()
=> $"({X}, {Y}, {Z})";
}
在 struct 前添加 readonly
关键字可以将结构定义为只读的。
添加后使用 distance 字段的地方会有错误提示: 只读结构的实例字段必须为只读。 。
删除 distance 后的代码如下:
public readonly struct Point3D
{
private static Point3D origin = new Point3D(0, 0, 0);
public static ref readonly Point3D Origin => ref origin;
public double X { get; }
public double Y { get; }
public double Z { get; }
public Point3D(double x, double y, double z)
{
X = x;
Y = y;
Z = z;
}
public double ComputeDistance()
{
return Math.Sqrt(X * X + Y * Y + Z * Z);
}
public static Point3D Translate(in Point3D source, double dX, double dY, double dZ) =>
new Point3D(source.X + dX, source.Y + dY, source.Z + dZ);
public override string ToString()
=> $"({X}, {Y}, {Z})";
}
使用 readonly struct
修饰符有两个好处:
- 它能暴露出结构中的意外修改;
- 它可以知道结构是不可变的,从而避免创建 防御副本(defensive copy)。
防御副本(defensive copy)
关于什么是 防御副本(defensive copy) ,通过对比上面两个版本对应的 IL 代码可以很明显的看出来。
不使用 readonly
修饰符时 Point3D.Translate 方法的 IL 代码:
.method /*06000007*/ public hidebysig static
valuetype ReadonlyStructs.Point3D/*02000002*/
Translate([in] valuetype ReadonlyStructs.Point3D/*02000002*/& source,
float64 dX,
float64 dY,
float64 dZ) cil managed
// SIG: 00 04 11 08 10 11 08 0D 0D 0D
{
.param [1]/*08000005*/
.custom /*0C000016:0A00000D*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.IsReadOnlyAttribute/*01000011*/::.ctor() /* 0A00000D */ = ( 01 00 00 00 )
// 方法在 RVA 0x20fc 处开始
// 代码大小 54 (0x36)
.maxstack 4
.locals /*11000002*/ init (valuetype ReadonlyStructs.Point3D/*02000002*/ V_0)
IL_0000: /* 02 | */ ldarg.0
IL_0001: /* 71 | (02)000002 */ ldobj ReadonlyStructs.Point3D/*02000002*/
IL_0006: /* 0A | */ stloc.0
IL_0007: /* 12 | 00 */ ldloca.s V_0
IL_0009: /* 28 | (06)000002 */ call instance float64 ReadonlyStructs.Point3D/*02000002*/::get_X() /* 06000002 */
IL_000e: /* 03 | */ ldarg.1
IL_000f: /* 58 | */ add
IL_0010: /* 02 | */ ldarg.0
IL_0011: /* 71 | (02)000002 */ ldobj ReadonlyStructs.Point3D/*02000002*/
IL_0016: /* 0A | */ stloc.0
IL_0017: /* 12 | 00 */ ldloca.s V_0
IL_0019: /* 28 | (06)000003 */ call instance float64 ReadonlyStructs.Point3D/*02000002*/::get_Y() /* 06000003 */
IL_001e: /* 04 | */ ldarg.2
IL_001f: /* 58 | */ add
IL_0020: /* 02 | */ ldarg.0
IL_0021: /* 71 | (02)000002 */ ldobj ReadonlyStructs.Point3D/*02000002*/
IL_0026: /* 0A | */ stloc.0
IL_0027: /* 12 | 00 */ ldloca.s V_0
IL_0029: /* 28 | (06)000004 */ call instance float64 ReadonlyStructs.Point3D/*02000002*/::get_Z() /* 06000004 */
IL_002e: /* 05 | */ ldarg.3
IL_002f: /* 58 | */ add
IL_0030: /* 73 | (06)000005 */ newobj instance void ReadonlyStructs.Point3D/*02000002*/::.ctor(float64,
float64,
float64) /* 06000005 */
IL_0035: /* 2A | */ ret
} // end of method Point3D::Translate
使用 readonly
修饰符时 Point3D.Translate 方法的 IL 代码:
.method /*06000007*/ public hidebysig static
valuetype ReadonlyStructs.Point3D/*02000002*/
Translate([in] valuetype ReadonlyStructs.Point3D/*02000002*/& source,
float64 dX,
float64 dY,
float64 dZ) cil managed
// SIG: 00 04 11 08 10 11 08 0D 0D 0D
{
.param [1]/*08000005*/
.custom /*0C000017:0A00000B*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.IsReadOnlyAttribute/*0100000C*/::.ctor() /* 0A00000B */ = ( 01 00 00 00 )
// 方法在 RVA 0x20c8 处开始
// 代码大小 30 (0x1e)
.maxstack 8
IL_0000: /* 02 | */ ldarg.0
IL_0001: /* 28 | (06)000002 */ call instance float64 ReadonlyStructs.Point3D/*02000002*/::get_X() /* 06000002 */
IL_0006: /* 03 | */ ldarg.1
IL_0007: /* 58 | */ add
IL_0008: /* 02 | */ ldarg.0
IL_0009: /* 28 | (06)000003 */ call instance float64 ReadonlyStructs.Point3D/*02000002*/::get_Y() /* 06000003 */
IL_000e: /* 04 | */ ldarg.2
IL_000f: /* 58 | */ add
IL_0010: /* 02 | */ ldarg.0
IL_0011: /* 28 | (06)000004 */ call instance float64 ReadonlyStructs.Point3D/*02000002*/::get_Z() /* 06000004 */
IL_0016: /* 05 | */ ldarg.3
IL_0017: /* 58 | */ add
IL_0018: /* 73 | (06)000005 */ newobj instance void ReadonlyStructs.Point3D/*02000002*/::.ctor(float64,
float64,
float64) /* 06000005 */
IL_001d: /* 2A | */ ret
} // end of method Point3D::Translate
对比两段代码可以发现,后面的 IL 代码每个获取 Point3D 成员的步骤比前面的都少了 ldobj
、 stloc.0
和 ldloca.s
三个操作。其中 ldobj
指令的作用是: 将地址指向的值类型对象复制到计算堆栈的顶部 。这个操作应该就是官方文档中提到的 防御副本(defensive copy) 。
另外测试时还发现:在 C# 8.0 (.NET Core 3.0) 中,即使不使用 readonly
修饰符,编译器也不会创建 防御副本(defensive copy) 。
这个估计是 8.0 中编译器又做了优化。其 IL 代码如下:
.method /*06000007*/ public hidebysig static
valuetype ReadonlyStructs.Point3D/*02000002*/
Translate([in] valuetype ReadonlyStructs.Point3D/*02000002*/& source,
float64 dX,
float64 dY,
float64 dZ) cil managed
// SIG: 00 04 11 08 10 11 08 0D 0D 0D
{
.param [1]/*08000005*/
.custom /*0C000019:0A00000D*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.IsReadOnlyAttribute/*01000011*/::.ctor() /* 0A00000D */ = ( 01 00 00 00 )
// 方法在 RVA 0x20fc 处开始
// 代码大小 30 (0x1e)
.maxstack 8
IL_0000: /* 02 | */ ldarg.0
IL_0001: /* 28 | (06)000002 */ call instance float64 ReadonlyStructs.Point3D/*02000002*/::get_X() /* 06000002 */
IL_0006: /* 03 | */ ldarg.1
IL_0007: /* 58 | */ add
IL_0008: /* 02 | */ ldarg.0
IL_0009: /* 28 | (06)000003 */ call instance float64 ReadonlyStructs.Point3D/*02000002*/::get_Y() /* 06000003 */
IL_000e: /* 04 | */ ldarg.2
IL_000f: /* 58 | */ add
IL_0010: /* 02 | */ ldarg.0
IL_0011: /* 28 | (06)000004 */ call instance float64 ReadonlyStructs.Point3D/*02000002*/::get_Z() /* 06000004 */
IL_0016: /* 05 | */ ldarg.3
IL_0017: /* 58 | */ add
IL_0018: /* 73 | (06)000005 */ newobj instance void ReadonlyStructs.Point3D/*02000002*/::.ctor(float64,
float64,
float64) /* 06000005 */
IL_001d: /* 2A | */ ret
} // end of method Point3D::Translate
既然 C# 8.0 中同样可以避免创建 防御副本(defensive copy) ,个人认为,不使用 readonly
修饰符,而是将目标框架修改为 .NET Core 3.0 更能提高 Point3D 结构的效率。
因为使用 readonly
修饰符时,必须删除非只读的 distance 字段,从而导致每次调用 ComputeDistance 方法都要重新计算,反而降低了效率。
顺便提一下,使用 ILDasm 命令反汇编时应指定对应的 dll 文件,而不是 exe 文件,否则会报如下错误。
C:\repos\ReadonlyStructs\ReadonlyStructs\bin\Debug\netcoreapp3.0>ILDasm /ALL /METADATA /OUT=ReadonlyStructs.ILDasm.New.8.0.log ReadonlyStructs.exe
错误: 'ReadonlyStructs.exe' 没有有效的 CLR 头,无法反汇编
ref struct
ref struct
声明,指示结构类型直接访问托管的内存,且必须始终分配有堆栈。
通俗点来说就是: ref struct
只能分配在栈中,需要装箱的操作 或 会导致装箱的使用方法 都不允许 。
定义方法很简单,在 struct
前添加 ref
修饰符即可。
public ref struct Point3D
{
public double X { get; }
public double Y { get; }
public double Z { get; }
public Point3D(double x, double y, double z)
{
X = x;
Y = y;
Z = z;
}
}
下面是摘自 MSDN 的说明。
Ref 结构类型
将
ref
修饰符添加到struct
声明定义了该类型的实例必须为堆栈分配。换言之,永远不能在作为另一类的成员的堆上创建这些类型的实例。此功能的主要动机是Span<T>
和相关结构。
保持ref struct
类型作为堆栈分配的变量的目标引入了几条编译器针对所有ref struct
类型强制执行的规则。
- 不能对
ref struct
装箱。无法向属于object
、dynamic
或任何接口类型的变量分配ref struct
类型。ref struct
类型不能实现接口。- 不能将
ref struct
声明为类或常规结构的字段成员。这包括声明自动实现的属性,后者会创建一个由编译器生成的支持字段。- 不能声明异步方法中属于
ref struct
类型的本地变量。不能在返回类似Task
、Task<TResult>
或Task
类型的同步方法中声明它们。- 无法在迭代器中声明
ref struct
本地变量。- 无法捕获 Lambda 表达式或本地函数中的
ref struct
变量。这些限制可确保不会以可提升至托管堆的方式意外地使用
ref struct
。
可以组合修饰符以将结构声明为readonly ref
。readonly ref struct
兼具ref struct
和readonly struct
声明的优点和限制。
非尾随命名参数
7.2 之前,方法调用的 命名参数 必须位于 位置参数 后面。如:
PrintOrderDetails("Gift Shop", 31, productName: "Red Mug");
7.2 及之后可以在 命名参数 的后面使用 位置参数 ,但前提是 位置参数必须处于正确的位置 。如:
PrintOrderDetails(sellerName: "Gift Shop", 31, productName: "Red Mug");
放在任何 无序命名参数 后面的 位置参数 是无效的。如:
// This generates CS1738: Named argument specifications must appear after all fixed arguments have been specified.
// 命名参数“productName”的使用位置不当,但后跟一个未命名参数
PrintOrderDetails(productName: "Red Mug", 31, "Gift Shop");
private protected 访问修饰符
在 我的这篇博客 中整理过 CLR 的类修饰符,其中一种 family and assembly 在 C# 中是没有提供的。
C# 7.2 中新增的 private protected
修饰符提供的正是这个功能,表示 成员可由派生类型访问,但这些派生类型必须在同一个程序集中定义。
条件 ref 表达式
条件表达式可能生成 ref 结果而不是值。增强了 条件表达式 的功能。示例如下:
var smallArray = new int[] { 1, 2, 3, 4, 5 };
var largeArray = new int[] { 10, 20, 30, 40, 50 };
int index = 7;
ref int refValue = ref ((index < 5) ? ref smallArray[index] : ref largeArray[index - 5]);
refValue = 0;
index = 2;
((index < 5) ? ref smallArray[index] : ref largeArray[index - 5]) = 100;
Console.WriteLine(string.Join(" ", smallArray));
Console.WriteLine(string.Join(" ", largeArray));