首页 > 解决方案 > 这个循环是否有可能无法运行?

问题描述

最近出现了一个问题,这对我来说是一次学习经历。类似以下内容给出了“使用未定义”错误:

int a;
for(int i = 0; i < 1; i++)
  a = 2;
a /= 2;

这是一个人为的例子,没有意义,但它给出了所需的错误。我知道使用内部范围来设置变量值是完全可以的,只要编译器可以计算出所有流程都会导致明确的分配:

int a;
if(someboolean)
  a=2;
else
  a=4;

但我以前没有意识到取决于某个变量值的内部范围块会出错,即使没有可察觉的变量可能是“错误”的方式:

int a;
bool alwaysTrue = true;
if(alwaysTrue)
  a = 2;
a /= 2; //error

用编译时间常数解决这个问题很好:

int a;
if(true)
  a = 2;
a /= 2; //fine

我想知道这是否可能是因为编译器完全删除了 if,但更复杂的语句也可以:

int a;
for(int i = 0; true; i++){
  a = 2;
  if(i >= 10)
    break;
}
a /= 2; //fine

也许这也被内联/优化了,但我的问题的本质是,对于第一个简单循环for(int i = 0; i < 1; i++),实际上是否有任何可以想象的方式使循环不会运行,因此“变量 a 可能未分配”是一个有效的断言,或者静态流分析是否只是在一个简单的“任何设置变量的条件控制代码块a被自动认为存在它可能无法运行的情况,我们直接捷径显示后续使用错误”规则上运行?

标签: c#variable-assignmentlanguage-specifications

解决方案


实际上是否有任何可以想象的方式使循环不会运行,因此“变量 a 可能未分配”是一个有效的断言

在您的示例中,假设a是局部变量,则必须运行循环。局部变量不能被修改,除非在它们被实例化的线程中。只是编译器不需要确定是这种情况,也不会。

我会指出,您的最后一个示例不是优化案例。它的工作原理就像while (true)您已经建立的情况一样,允许编译器将变量视为明确分配的。

就“为什么”而言,有两种方法可以解释这个问题。简单的方法是“为什么编译器要这样做?” 答案是“因为语言规范是这样说的”。

语言规范并不总是最容易阅读的东西,明确分配的规则是该陈述的一个特别鲜明的例子,但您可以在此处找到对“为什么”的第一个解释的答案: https ://docs.microsoft .com/en-us/dotnet/csharp/language-reference/language-specification/variables#precise-rules-for-determining-defined-assignment

您会注意到,一般来说,循环控制结构导致明确分配的唯一方法是控制循环本身的表达式是否参与明确分配。这达到了“在真实表达式之后确定分配”“在错误表达之后确定分配”子状态场景。您还会注意到规范的这一部分不适用于您的示例。

所以你留下了循环的明确分配规则的要点(还有其他限定条件,但在简单的情况下不适用):

v在expr的开头与在stmt的开头具有相同的明确赋值状态。

即无论v在循环之前是什么,在循环之后都是一样的。循环本身被忽略。

那么,如果循环通常不会创建明确的分配,为什么由文字值(即“常量表达式”)控制的循环允许明确的分配?这是因为规范的不同部分,由明确分配的规则引用:https ://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#end-points -和可达性

流分析考虑了控制语句行为的常量表达式(Constant expressions)的值,​​但不考虑非常量表达式的可能值。

进行流分析以确定语句或循环端点的可达性,但这直接适用于明确的分配:

  • v在块、checkeduncheckedifwhiledoforforeachlockusing或语句的终点处的明确switch分配状态是通过检查以该语句终点为目标的所有控制流传输上的v的明确分配状态来确定的。如果在所有此类控制流传输上明确分配了 v,则语句的末尾明确分配了v 。否则; v在语句的末尾没有明确地赋值。可能的控制流转移集合的确定方式与检查语句可达性的方式相同 [强调我的]

换句话说,编译器在确定明确赋值时将应用它用于语句可达性的相同分析。因此,由常量表达式控制的循环会被分析,而那些不是的则不会。

解释“为什么”的更难的方法是“为什么语言作者要这样写规范?” 这就是您开始获得基于意见的答案的地方,除非您实际上是在与一位语言作者交谈(事实上,他可能会在某个时候发布答案,所以......并不是遥不可及的可能性:))。

但是,在我看来,有几种方法可以解决这个问题:

  • 他们可能是这样写规范的,因为就像现在明确的赋值规则一样复杂,如果编译器需要对变量进行静态流分析,它们会更加复杂,更不用说实际编写编译器要复杂得多本来可以。
  • 从理论上讲,它归结为停机问题。即,一旦您开始要求编译器进行重要的流分析,您就为某人编写了一些 C# 代码打开了大门,从而有效地使编译器确定 C# 代码是否可以停止。由于不可能在所有情况下都这样做,因此在规范中包含该要求可能是个坏主意。

处理不仅可以而且必须在编译时计算的常量表达式是一回事。让编译器基本上运行你的程序只是为了编译它,是一个完整的“另一个球”蜡。


推荐阅读