Skip to content

C# 7.2 中的新增功能

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

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

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.