Dan*_*Tao 5 .net heap stack reference value-type
我知道这是一个实现细节.我真的很好奇微软的CLR 中的实现细节是什么.
现在,请耐心等级,因为我没有在大学学习CS,所以我可能错过了一些基本原则.
但是,我认为现在对CLR中实现的"堆栈"和"堆"的理解是可靠的.例如,我不打算做一些不准确的伞语句,例如"值类型存储在堆栈中".但是,在最常见的场景-普通的香草局部变量,值类型作为参数通过或声明的方法中,并没有包含在瓶盖内-值类型变量都保存在栈上(同样,微软的CLR).
我想我不确定的是ref值类型参数的来源.
最初我的想法是,如果调用堆栈看起来像这样(左=底部):
A() -> B() -> C()
Run Code Online (Sandbox Code Playgroud)
...然后在A范围内声明并作为ref参数传递给B的局部变量仍然可以存储在堆栈中 - 不是吗?B只需要在A的框架中存储该局部变量的内存位置(如果这不是正确的术语,请原谅我;无论如何,我认为我的意思很清楚).
我意识到这不可能是严格正确的,但是,当我想到我能做到这一点时:
delegate void RefAction<T>(ref T arg);
void A()
{
int x = 100;
RefAction<int> b = B;
// This is a non-blocking call; A will return immediately
// after this.
b.BeginInvoke(ref x, C, null);
}
void B(ref int arg)
{
// Putting a sleep here to ensure that A has exited by the time
// the next line gets executed.
Thread.Sleep(1000);
// Where is arg stored right now? The "x" variable
// from the "A" method should be out of scope... but its value
// must somehow be known here for this code to make any sense.
arg += 1;
}
void C(IAsyncResult result)
{
var asyncResult = (AsyncResult)result;
var action = (RefAction<int>)asyncResult.AsyncDelegate;
int output = 0;
// This variable originally came from A... but then
// A returned, it got updated by B, and now it's still here.
action.EndInvoke(ref output, result);
// ...and this prints "101" as expected (?).
Console.WriteLine(output);
}
Run Code Online (Sandbox Code Playgroud)
那么在上面的例子中,x(在A的范围内)存储在哪里?这是如何工作的?盒装了吗?如果不是,它现在是否受垃圾收集,尽管它是一个值类型?或者可以立即回收记忆?
我为这个冗长的问题道歉.但即使答案很简单,也许这将为那些发现自己在未来想到同样事情的人提供信息.
我不相信当您使用BeginInvoke()andEndInvoke()与reforout参数时,您确实是通过 ref 传递变量。EndInvoke()我们还必须使用参数进行调用这一事实ref应该是对此的线索。
让我们更改您的示例来演示我所描述的行为:
void A()
{
int x = 100;
int z = 400;
RefAction<int> b = B;
//b.BeginInvoke(ref x, C, null);
var ar = b.BeginInvoke(ref x, null, null);
b.EndInvoke(ref z, ar);
Console.WriteLine(x); // outputs '100'
Console.WriteLine(z); // outputs '101'
}
Run Code Online (Sandbox Code Playgroud)
如果您现在检查输出,您会发现 的值x实际上没有变化。但z 现在包含更新值。
ref我怀疑当您使用异步 Begin/EndInvoke 方法时,编译器会改变传递变量的语义。
查看此代码生成的 IL 后,似乎仍然传递了ref参数。虽然 Reflector 没有显示此方法的 IL,但我怀疑它只是不将参数作为参数传递,而是在幕后创建一个单独的变量来传递给. 当您随后调用时,必须再次提供参数以从异步状态检索值。这些参数很可能实际上存储为最终检索其值所需的对象的一部分(或与其结合)。BeginInvoke()by refrefB()EndInvoke()refIAsyncResult
让我们思考一下为什么这种行为可能会以这种方式起作用。当您对方法进行异步调用时,您是在单独的线程上执行此操作。该线程有自己的堆栈,因此不能使用别名变量的典型机制ref/out。但是,为了从异步方法获取任何返回值,您最终需要调用EndInvoke()来完成操作并检索这些值。但是,对方法的调用EndInvoke()可能很容易发生在与原始调用BeginInvoke()或方法的实际主体完全不同的线程上。显然,调用堆栈不是存储此类数据的好地方 - 特别是因为一旦异步操作完成,用于异步调用的线程可能会重新用于不同的方法。因此,需要除堆栈之外的某种机制来将返回值和 out/ref 参数从被回调的方法“编组”到最终使用它们的站点。
我相信这个机制(在 Microsoft .NET 实现中)就是IAsyncResult对象。事实上,如果您IAsyncResult在调试器中检查该对象,您会注意到在非公共成员中存在_replyMsg,其中包含一个Properties集合。该集合包含类似 和 的元素__OutArgs,__Return其数据似乎反映了它们的同名元素。
编辑: 这是我想到的关于异步委托设计的理论。BeginInvoke()和 的签名似乎EndInvoke()被选择为尽可能相似,以避免混淆并提高清晰度。该BeginInvoke()方法实际上不需要接受参数ref/out- 因为它只需要它们的值......而不是它们的标识(因为它永远不会将任何东西分配给它们)。BeginInvoke()然而,如果一个调用需要一个 ,int另一个EndInvoke()调用需要一个 ,这确实很奇怪(例如)ref int。现在,可能存在技术原因为什么开始/结束调用应该具有相同的签名 - 但我认为清晰度和对称性的好处足以验证这样的设计。
当然,所有这些都是 CLR 和 C# 编译器的实现细节,并且将来可能会发生变化。然而,有趣的是,如果您期望传递给的原始变量实际上会被修改,则可能会出现混淆BeginInvoke()。EndInvoke()它还强调了调用完成异步操作的重要性。
也许 C# 团队的某人(如果他们看到这个问题)可以提供有关此功能背后的细节和设计选择的更多见解。