为什么C#编译器不会阻止属性引用自身?

37

如果我这么做,就会得到一个System.StackOverflowException异常:

private string abc = "";
public string Abc
{
    get
    { 
        return Abc; // Note the mistaken capitalization
    }
}

我理解这个问题的原因是该属性引用了它自己,导致无限循环(请参见之前的问题这里这里)。

但我想知道的是(之前的问题中没有回答),为什么C#编译器没有捕获到这个错误呢?编译器会检查其他一些循环引用的情况(如类从自身继承等),对吗?难道只是这个错误不常见,不值得检查吗?或者有没有我没有考虑到的情况,需要让属性实际上以这种方式引用自己?


2
通过“引用自身”,我想我真正的意思是“递归调用自身”,但你明白我的意思。 - Tim Goodman
6个回答

46
您可以在此链接中看到“官方”原因的最后评论。
微软感谢您对Visual Studio的建议!您说得对,我们可以轻松检测到属性递归,但是我们不能保证递归中没有完成任何有用的任务。属性的主体可能会设置对象上的其他字段,这些字段会改变下一个递归的行为,基于来自控制台的用户输入更改其行为,甚至还可以根据随机值表现不同。在这些情况下,自递归属性确实可以终止递归,但我们无法在编译时确定是否是这种情况(而不解决停机问题!)。
出于以上原因(以及禁止此操作所需的破坏性更改),我们无法禁止自递归属性。
Alex Turner 程序经理 Visual C#编译器

21
不过,如果他们能生成一个关于此的编译器警告,那会很好。我认为那会很有价值。 - Nick
1
@Tim:这些副作用可能是通常不可见于用户的内部副作用。虽然属性 getter 产生副作用并不完全不可能(例如,如果您想要计算 get 被调用的次数),但这仍然相当愚蠢 :) - Brian
3
很多商店都非常严格,不允许任何带有警告的编译内容通过,因此对于一些可能不需要警告的事情,它们非常严格地产生警告。请参见http://blogs.msdn.com/ericlippert/archive/2010/01/25/why-are-unused-using-directives-not-a-warning.aspx。 - Tanzelax
@Tanzelax - 确实如此,但我发现你引用链接中的示例很奇怪,因为他们明确提到“无用代码”不值得警告,但是如果您创建一个从未赋值的成员变量foo,则会产生警告“The field foo is never used”。 - Nick
@Nick:Eric在这个帖子中发布了另一个答案,基本上解决了这个问题。“代码是有效的代码...但由于这是无意义的代码,你可能不是想这样做。”你可以直接问他。 :) - Tanzelax
显示剩余3条评论

14

除了Alex的解释之外,另一个要点是我们试图对那些可能会做出你并不想要的操作的代码发出警告,这样你就不会意外地发布带有错误的代码

在这种情况下,警告实际上能够节省多少时间?仅是一次测试运行。你会在测试代码时立即发现这个错误,因为它会立即崩溃并死亡。在这里,警告实际上无法给你带来太多的好处。递归属性评估中存在一些微妙错误的可能性很低。

相比之下,如果你像这样做某些事情,我们确实会发出警告:

int customerId; 
...
this.customerId= this.customerId;

这段代码不会导致程序崩溃,且是有效的代码;它将一个值分配给了一个字段。但由于这是一段毫无意义的代码,你可能并不想这样做。由于它不会导致程序崩溃,我们会发出警告,提示你可能没有意识到这里有些东西,并且可能无法通过程序崩溃来发现。


1
@Nick:我没有看到矛盾之处。(这并不意味着没有矛盾: 我很大,我包含万象。)我只是不明白你的意思。 - Eric Lippert
@Eric Lippert:谢谢你提到《我自己之歌》。:-) - jason
2
@Nick:这两种情况的区别在于,在“未使用using”示例中,未使用的using可能只是一些无意义的东西。它根本没有任何意义,从未打算有意义,可以删除或保留而不会造成语义变化。“将变量分配给自身”的警告几乎肯定是打算表达其他含义。我们不仅仅是指出代码是无用的,我们还指出代码可能旨在具有目的性但实际上是无用的,这两者的结合极有可能是一个错误。 - Eric Lippert
不,我明白了。我只是那种非常喜欢编译器帮助我的人之一。这里有一个在VB.NET中总是让我感到困扰的编译器警告情况:https://dev59.com/SUzSa4cB1Zd3GeqPkjiM - Nick
即使不是必然的错误,属性递归几乎肯定是一种不良惯例。我也不同意Eric的说法,认为警告只会节省几秒钟的时间。我确信即使是经验丰富的程序员在StackOverflowException突然出现的位置可能会被卡住几分钟。一个简单的警告可以在这种情况下为许多开发人员节省宝贵的时间,这很容易检测,并且几乎肯定是一个错误。 - MgSam
显示剩余6条评论

10

引用自身的属性并不总是导致无限递归和堆栈溢出。例如,以下代码可以正常工作:

int count = 0;
public string Abc
{
    count++;
    if (count < 1) return Abc;
    return "Foo";
}

以上是一个虚拟的例子,但我相信人们可以想出类似的有用的递归代码。编译器无法确定是否会发生无限递归(停机问题)。

在简单情况下生成警告将是有帮助的。


1

他们可能认为这样做会使编译器变得不必要的复杂,而没有任何真正的收益。

第一次调用此属性时,您很容易发现这个拼写错误。


1

首先,你会收到一个未使用变量abc的警告。

其次,递归本身并没有什么不好的,只要它不是无限递归。例如,代码可能会调整一些内部变量,然后递归调用相同的getter。然而,对于编译器来说,很难证明某些递归是无限的还是有限的(这至少是一个NP问题)。编译器可以捕捉一些简单的情况,但是消费者会感到惊讶,因为更复杂的情况能够通过编译器的检查。


0

它检查的其他情况(除了递归构造函数)都是无效的IL。
此外,所有这些情况,即使是递归构造函数,也保证会失败。

然而,有可能(虽然不太可能)有意创建一个有用的递归属性(使用if语句)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接