这是传递给带有in关键字的方法的只读结构的防御性副本

Mik*_*ike 6 c# cil .net-core

我正在尝试将a传递readonly struct给带有in修饰符的方法。当我查看生成的IL代码时,似乎已经构造了只读结构的防御性副本。

readonly struct被定义为

public readonly struct ReadonlyPoint3D
{
    public ReadonlyPoint3D(double x, double y, double z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public double X { get; }
    public double Y { get; }
    public double Z { get; }
}
Run Code Online (Sandbox Code Playgroud)

接受的方法 ReadonlyPoint3D

private static double CalculateDistance(in ReadonlyPoint3D point1, in ReadonlyPoint3D point2)
{
    double xDifference = point1.X - point2.X;
    double yDifference = point1.Y - point2.Y;
    double zDifference = point1.Z - point2.Z;

    return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}
Run Code Online (Sandbox Code Playgroud)

以及我调用此方法的方式:

static void Main(string[] args)
{
    var point1 = new ReadonlyPoint3D(0, 0, 0);
    var point2 = new ReadonlyPoint3D(1, 1, 1);

    var distance = CalculateDistance(in point1, in point2);
}

Run Code Online (Sandbox Code Playgroud)

如果查看为CalculateDistance方法调用生成的IL ,我会看到ReadonlyPoint3D实例是通过引用传递的

IL_0045: ldloca.s     point1
IL_0047: ldloca.s     point2
IL_0049: call         float64 CSharpTests.Program::CalculateDistance(valuetype CSharpTests.ReadonlyPoint3D&, valuetype CSharpTests.ReadonlyPoint3D&)
IL_004e: stloc.2      // distance
Run Code Online (Sandbox Code Playgroud)

但是,CalculateDistance方法的IL似乎会复制point1point2参数:

// [25 9 - 25 10]
IL_0000: nop

// [26 13 - 26 54]
IL_0001: ldarg.0      // point1
IL_0002: call         instance float64 CSharpTests.ReadonlyPoint3D::get_X()
IL_0007: ldarg.1      // point2
IL_0008: call         instance float64 CSharpTests.ReadonlyPoint3D::get_X()
IL_000d: sub
IL_000e: stloc.0      // xDifference

// the resit is omitted for the sake of brevity, essentially same code repeated for Y & Z
Run Code Online (Sandbox Code Playgroud)

ldarg.0ldarg.1in CalculateDistance方法生成的IL使我认为是point1&的副本point2。我期望在这里看到的是ldloca.s我认为要加载point1&地址的指令point2

我是否正确理解并制作了防御性副本?还是我对IL代码的解释是错误的?

我在C#7.3中使用.NET Core 2.1


编辑

根据Microsoft文档,使用in修饰符传递的可变结构将创建防御性副本

如果我定义了可变结构

public struct MutablePoint3D
{
    public MutablePoint3D(double x, double y, double z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public double X { get; set; }
    public double Y { get; set; }
    public double Z { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

并通过 in

private static double CalculateDistance(in MutablePoint3D point1, in MutablePoint3D point2)
{
    double xDifference = point1.X - point2.X;
    double yDifference = point1.Y - point2.Y;
    double zDifference = point1.Z - point2.Z;

    return Math.Sqrt(xDifference * xDifference + yDifference * yDifference + zDifference * zDifference);
}
Run Code Online (Sandbox Code Playgroud)

我可以看到生成的IL代码类似于生成的代码readonly struct

// [26 13 - 26 54]
IL_0001: ldarg.0      // point1
IL_0002: call         instance float64 CSharpTests.MutablePoint3D::get_X()
IL_0007: ldarg.1      // point2
IL_0008: call         instance float64 CSharpTests.MutablePoint3D::get_X()
IL_000d: sub
IL_000e: stloc.0      // xDifference
// the resit is omitted for the sake of brevity
Run Code Online (Sandbox Code Playgroud)

另一个观察结果是,如果我inCalculateDisctance接受方法中删除修饰符ReadonlyPoint3D,则生成的IL代码正是我所期望的

// [35 13 - 35 54]
IL_0001: ldarga.s     point1
IL_0003: call         instance float64 CSharpTests.ReadonlyPoint3D::get_X()
IL_0008: ldarga.s     point2
IL_000a: call         instance float64 CSharpTests.ReadonlyPoint3D::get_X()
IL_000f: sub
IL_0010: stloc.0      // xDifference
Run Code Online (Sandbox Code Playgroud)

但这似乎与Microsoft Docs中的建议不符


编辑2

正如@PetSerAl在评论中所建议的,sharplab.io为该代码生成了不同的IL。差异- ldobj仅针对的指令CalculateDistance(in MutablePoint3D point1, in MutablePoint3D point2)将说明仅针对这种情况进行了防御性复制。

但是,问题中张贴的IL指令取自ReSharper的IL Viewer,并通过ILDASM.exe工具进行了验证(用于Release配置,如sharplab.io)。因此,我不确定这种差异来自何处以及哪个输出值得信任。

Mik*_*ike 3

长篇讨论可以在相关的 GitHub 问题上找到。

本质上,这是一个已修复的Roslyn bug,最新版本的 VS 2019(16.2 及更高版本)已修复。