Skip to content

C# 7.2 中的新增功能

🏷️ C# C# 新增功能

官方文档见 这里。本文在其基础上添加了一些自己理解及示例代码,如有不正确的地方欢迎指正。

C# 7.2 又是一个单点版本,它增添了大量有用的功能。
此版本的一项主要功能是避免不必要的复制或分配,进而更有效地处理值类型。
其余功能很微小,但值得拥有。

语言版本选择配置

C# 7.2 使用 语言版本选择 配置元素来选择编译器语言版本。

在 VS 2017 中你可以通过 项目属性 => 生成 => 高级 => 语言版本 来设置项目使用的 C# 版本。
但在 VS 2019 中创建的 .NET Core 3.0 项目中你会发现该选项已变灰且显示为 已根据框架版本自动选择 。点击 为何无法选择其它 C# 版本? 会跳转到 MSDN

这里可以看到 目标框架各个版本 对应的 C# 语言版本的默认值。

目标框架versionC# 语言版本的默认值
.NET Core3.xC# 8.0
.NET Core2.xC# 7.3
.NET Standard2.1C# 8.0
.NET Standard2.0C# 7.3
.NET Standard1.xC# 7.3
.NET Framework全部C# 7.3

其中 .NET Core 3.0 对应的默认值就是 C# 8.0

如果必须修改项目的语言版本,需要手动修改项目文件。

修改方法:添加如下元素到项目文件(详情见 这里)。

xml
<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 修饰符,指定形参通过引用传递,但不通过调用方法修改。**示例如下:

csharp
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 不能被修改,尝试修改会报错。

csharp
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 修饰符。

csharp
start = Point3D.Translate(in start, 5, 5, 5);

针对方法返回的 ref readonly 修饰符,指示方法通过引用返回其值,但不允许写入该对象。

使用方法是在方法定义的 ref 后添加 readonly 修饰符。

csharp
public static ref readonly Point3D Origin => ref origin;

同样调用的地方也需要在 ref 后添加 readonly 修饰符。

csharp
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 代码中看到,后面会有介绍)。

csharp
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 后的代码如下:

csharp
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 修饰符有两个好处:

  1. 它能暴露出结构中的意外修改;
  2. 它可以知道结构是不可变的,从而避免创建 防御副本(defensive copy)

防御副本(defensive copy)

关于什么是 防御副本(defensive copy) ,通过对比上面两个版本对应的 IL 代码可以很明显的看出来。

不使用 readonly 修饰符时 Point3D.Translate 方法的 IL 代码:

csharp
.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 代码:

csharp
.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 成员的步骤比前面的都少了 ldobjstloc.0ldloca.s 三个操作。其中 ldobj 指令的作用是: 将地址指向的值类型对象复制到计算堆栈的顶部 。这个操作应该就是官方文档中提到的 防御副本(defensive copy)

另外测试时还发现:在 C# 8.0 (.NET Core 3.0) 中,即使不使用 readonly 修饰符,编译器也不会创建 防御副本(defensive copy)
这个估计是 8.0 中编译器又做了优化。其 IL 代码如下:

csharp
.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 文件,否则会报如下错误。

bash
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 修饰符即可。

csharp
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 装箱。无法向属于 objectdynamic 或任何接口类型的变量分配 ref struct 类型。
  • ref struct 类型不能实现接口。
  • 不能将 ref struct 声明为类或常规结构的字段成员。这包括声明自动实现的属性,后者会创建一个由编译器生成的支持字段。
  • 不能声明异步方法中属于 ref struct 类型的本地变量。不能在返回类似 TaskTask<TResult>Task 类型的同步方法中声明它们。
  • 无法在迭代器中声明 ref struct 本地变量。
  • 无法捕获 Lambda 表达式或本地函数中的 ref struct 变量。

这些限制可确保不会以可提升至托管堆的方式意外地使用 ref struct
可以组合修饰符以将结构声明为 readonly refreadonly ref struct 兼具 ref structreadonly struct 声明的优点和限制。

非尾随命名参数

7.2 之前,方法调用的 命名参数 必须位于 位置参数 后面。如:

csharp
PrintOrderDetails("Gift Shop", 31, productName: "Red Mug");

7.2 及之后可以在 命名参数 的后面使用 位置参数 ,但前提是 位置参数必须处于正确的位置 。如:

csharp
PrintOrderDetails(sellerName: "Gift Shop", 31, productName: "Red Mug");

放在任何 无序命名参数 后面的 位置参数 是无效的。如:

csharp
// 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 结果而不是值。增强了 条件表达式 的功能。示例如下:

csharp
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));