在 C# 中,为什么我不能使用其地址填充局部变量,然后再使用该变量?

In C#, why can't I populate a local variable using its address, then use the variable later?

考虑以下代码:

private unsafe void Function()
{
    int length;

    // This line raises error CS1686, "Local 'length' or its members cannot have their address taken and be used inside an anonymous method or lambda expression".
    glGetProgramiv(1, GL_PROGRAM_BINARY_LENGTH, &length);

    FunctionWithLambda(() => Console.WriteLine(length));
}

private void FunctionWithLambda(Action callback)
{
    callback();
}

请注意,我正在使用 length(局部变量)的地址,然后使用 变量本身不是 它的地址)在 lambda 中。我理解为什么不能在 lambda 中直接使用局部变量地址 (请参阅 , among other examples), but why can't I use the value of length once assigned (even if that assignment happens to use the & operator)? The official documentation for error CS1686 (https://docs.microsoft.com/bs-latn-ba/dotnet/csharp/misc/cs1686)尚未澄清这种混淆。

我的假设是这只是语言限制,但我很好奇是否存在我遗漏的潜在技术原因。另请注意,我不是在询问 如何解决此问题(我知道我可以先轻松地将 length 复制到另一个局部变量)。

The C# specification 表示如下 (我的粗体):

23.4 Fixed and moveable variables

The address-of operator (§23.6.5) and the fixed statement (§23.7) divide variables into two categories:
Fixed variables and moveable variables.

...snip...

The & operator (§23.6.5) permits the address of a fixed variable to be obtained without restrictions. However, because a moveable variable is subject to relocation or disposal by the garbage collector, the address of a moveable variable can only be obtained using a fixed statement (§23.7), and that address remains valid only for the duration of that fixed statement.

In precise terms, a fixed variable is one of the following:

  • A variable resulting from a simple-name (§12.7.3) that refers to a local variable, value parameter, or parameter array, unless the variable is captured by an anonymous function (§12.16.6.2).
  • .....

所以它被规范明确禁止。至于为什么是禁止的,这个你得问语言设计者,但考虑到捕获变量涉及的复杂程度,这有点合乎逻辑。

我想原因很简单:编译太复杂

编译器必须解决 2 个问题:

  1. 为匿名方法生成一个 clourse。
  2. 同步变量的值。

假设以下代码有效。

unsafe void Function()
{
    int length = 1;
    void bar() => Console.WriteLine(length);
    bar();
    foo(&length);
    bar();
}

unsafe void foo(int* i) { (*i)++; }

预期结果是:

1
2

为了解决第一个问题,C# 将生成一个匿名的 class 来保存上值。

这是伪代码:

class _Anonymous
{
    public int _length;

    public void _bar() { Console.WriteLine(_length); }
}

unsafe void Function()
{
    int length = 1;
    var a = new _Anonymous { _length = length };
    a._bar();
    foo(&length);
    a._bar();
}

为了解决第二个问题,C#使用生成的字段而不是原来的局部变量。

unsafe void Function()
{
    //int length = 1;
    var a = new _Anonymous { _length = 1 };
    a._bar();
    foo(&a._length);
    a._bar();
}

这些都是编译器可以做的工作。但是到现在代码还是不行,我们需要一个额外的固定块。

unsafe void Function()
{
    var a = new _Anonymous { _length = 1 };
    a._bar();
    fixed (int* p = &a._length)
        foo(p);
    a._bar();
}

所以可以使用更智能的编译器消除限制,但如果我们禁止此类代码,事情就会变得更容易。