这段代码如何引用结构?

时间:2017-09-05 21:31:41

标签: c# struct closures

我尝试了这段代码

UIResponder

并获得输出struct Bar { public int Value; } async Task doItLater(Action fn) { await Task.Delay(100); fn(); } void Main() { Bar bar = new Bar { Value = 1 }; //Bar is a struct doItLater(() => { Console.WriteLine(bar.Value); }).Wait(); } 。现在这让我很困惑。我的逻辑如下

  • Bar是一个结构。因此,所有实例都应存储在堆栈中
  • 1被命中时,该执行线程已完成,并且请求TPL稍后执行Task.Delay(100)
  • fn()存储在堆栈中,当我们在闭包中访问它时,该帧不应该存在。

那么我怎么得到bar的输出?

2 个答案:

答案 0 :(得分:7)

彼得的回答是正确的;总结:

  • 值类型不会“进入堆栈”。 变量在知道其生命周期很短的情况下进入堆栈。变量的类型无关紧要;当变量的生命周期不短时,包含int的变量会在堆上。如果知道其生命周期很短,则包含对字符串的引用的变量将进入堆栈。

  • await不会“终止一个帖子”。 async-await的重点是需要另一个线程!异步等待的重点是在等待异步操作完成时继续使用当前线程。如果您认为await与线程有任何关系,请阅读“没有线程”。它没有。

但是我想解决关于堆栈的基本错误,作为延续的具体化。

什么是延续?这只是一个奇特的词汇,“在这一点上,这个程序下一步要做什么?”

在正常的代码中 - 没有等待,没有收益,没有lambda,没有什么花哨 - 事情非常简单。当你有:

y = 123;
x = f();
g(x);
return y;

f的延续是“为x指定一个值并将其传递给g”,而g的延续是“运行我现在正在使用的任何方法的延续,赋予它y的值”。

如您所知,我们可以使用堆栈在正常程序中重新启用continuation。堆栈数据结构实际上维护了三件事:

  • 局部变量的状态 - 这是激活信息
  • 正常延续的代码地址 - “返回地址”
  • 足以在运行时计算异常延续的信息;也就是说,“接下来会发生什么?”抛出异常时。

但是这个数据结构只是一个堆栈,因为函数激活在逻辑上构成了正常程序中的堆栈。函数Foo调用Bar,然后Bar调用Blah,然后Blah返回Bar,Bar返回Foo。

现在让我们放弃皱纹:

int y = 123;
Func<int> f = () => y;
return f;

现在,即使在当前方法返回后,也必须保留局部y的值,因为可能会调用委托。 因此,y的生命周期长于当前激活的生命周期,并且不会进入堆栈

或者这个:

int y = 123;
yield return y;
yield return y;

现在必须在迭代器块的的MoveNext的调用中保留,所以同样,y必须是不在堆栈上的变量;它的使用寿命很长,所以它必须在堆上。但请注意,这比前一种情况更奇怪,因为方法的激活可以通过yield暂停,并由未来的MoveNext 恢复。现在我们有一种情况,方法调用不会在逻辑上形成堆栈,因此继续信息不能再在堆栈上

等待是一样的;我们再次遇到一个方法,一个方法可以暂停,其他东西可以在同一个线程上发生,然后以某种方式,该方法在它停止的地方恢复。

Await和yield都是名为 coroutines 的更通用功能的示例。普通方法可以做三件事:抛出,挂起或返回。一个协程可以做第四件事:暂停,稍后再恢复。

正常方法只是协同程序的一个特例;常规方法是不挂起的协同程序。由于它们具有此限制,因此普通方法可以使用堆栈作为其延续语义的具体化。 协同程序不。由于协程的激活不形成堆栈,因此堆栈不用于它们的本地变量激活记录;也不用于存储返回地址等延续信息。

你陷入了相信特殊情况 - 不停止的惯例 - 是世界必须如何的陷阱,但这根本不是真的。相反,非挂起方法是可以通过使用堆栈来存储其连续信息来优化的特殊方法。

答案 1 :(得分:3)

您的分析在多个方面错过了标记:

  

•Bar是一个结构。因此,所有实例都应存储在堆栈中

事实并非如此。值类型对象可以存储在堆栈中,但并非所有值类型都存储在堆栈中。请参阅Eric Lippert着名的The Stack Is An Implementation Detail文章。

在您的示例中,bar对象实际上存储在堆栈中,因为它是在传递给doItNow()的闭包中捕获的。编译器创建一个隐藏的类,其中存储bar对象,并且此类在堆上分配。所以bar对象本身也在堆上分配。

  

•当命中Task.Delay(100)时,该执行线程完成,并且请求TPL稍后执行fn()。

实际上,当await被击中时。简单地调用Task.Delay()只会创建并启动一个将在100毫秒内完成的新Task。直到await执行doItLater()方法返回。这与“执行线程” “完成”不同。该线程继续(在您的情况下,就Wait()方法返回的Task对象上的doItLater()调用而言)。

  

•bar存储在堆栈中,当我们在闭包中访问它时,该帧应该不存在。

因为你调用Wait(),即使bar对象存储在堆栈中是正确的,当执行doItLater()中的延续时,该堆栈帧仍然存在并且执行fn()委托调用。在Main()方法完成之前,Wait()方法无法返回,并且在doItLater()任务完全完成之前不会发生(包括调用fn委托被传递给它。)

换句话说,即使我们忽略了你所遇到的其他误解,在这种情况下也不会出现问题,因为bar对象无论如何都会存在。