使用 yield 作为上下文关键字是否会导致问题

Could using yield as a contextual keyword ever cause an issue

Essential C# 中指出:

After C# 1.0, no new reserved keywords were introduced to C#. However, some constructs in later versions use contextual keywords, which are significant only in specific locations. Outside these designated locations, contextual keywords have no special significance.* By this method, most C# 1.0 code is compatible with the later standards.

*For example, early in the design of C# 2.0, the language designers designated yield as a keyword, and Microsoft released alpha versions of the C# 2.0 compiler, with yield as a designated keyword, to thousands of developers. However, the language designers eventually determined that by using yield return rather than yield, they could ultimately avoid adding yield as a keyword because it would have no special significance outside its proximity to return.

现在我不明白这一点,就像在 c# 2.0 之前一样,使用 IEnumerable return 的每个方法都必须在其中包含一个 return 语句,而 yield 可以用作上下文关键字仅在 returned IEnumerable 但没有 return 语句的方法中。例如

public IEnumerable<int> GetInts()
{
    for (int i = 0; i < 1000; i++)
        yield i;
}

由于此方法不会编译 C# 2.0 之前的版本,我看不出这会如何破坏向后兼容性。

所以我的问题是:

是否存在在 C# 中使用 yield 而不是 yield return 会破坏向后兼容性或以其他方式导致问题的情况?

问题

for (int i = 0; i < 1000; i++)
    yield i;

如果没有 yield 关键字,这确实是无效的,但是如果我们在 i 周围添加括号呢?

for (int i = 0; i < 1000; i++)
    yield (i);

现在这是对名为 yield 的方法的完全有效调用。因此,如果我们将 yield (i); 解释为上下文关键字 yield 的使用,则此有效代码的含义将会改变,从而破坏向后兼容性。

更正式的看待这个问题的方式是这样的:如果我们更改 C# 2 的语法以用 statement: 'yield' expression ';' 替换 statement: 'yield' 'return' expression ';',那么该规则之间就会有歧义以及函数调用的规则,因为 expression 可以派生为 '(' expression ')' 并且 'yield' '(' expression ')' ';' 也可以是表达式语句中的函数调用。

可能的解决方案 1

你当然可以说只有 yield i;(或任何其他不以左括号开头的表达式)应该被解释为上下文关键字的使用,而 yield (i); 仍然是被视为方法调用。然而,这是非常不一致和令人惊讶的行为 - 在表达式周围添加括号不应该像那样改变语义。

这也意味着将上面的语法规则更改为类似 statement: 'yield' expressionNoStartingParen ';' 的内容,然后定义 expressionNoStartingParen,这将重复 expression 的大部分实际定义。这会使语法变得非常复杂(尽管您可以通过仅用文字而不是语法来描述无起始括号的要求来解决这个问题,然后在实际实现中使用标志来跟踪它(尽管这可能不是使用大多数解析器生成器的选项))。

可能的解决方案 2

您在评论中提到的解决这种歧义的另一种方法是仅在没有 return 的非 void 方法中将 yield expression; 解释为 yield 语句陈述。这将保持向后兼容性,因为这样的方法无论如何在 C# 1 中都是无效的。然而,这会有些不一致,因为现在您可以定义一个名为 yield 的方法,并在不使用 yield 语句的方法中调用它,而不是在使用 yield 语句的方法中调用它。

更重要的是,这不是上下文关键字通常的样子。通常,上下文关键字在标识符有效的任何地方使用时都充当标识符,并且只能在标识符不能出现的地方用作关键字。这里不会是这种情况。这不仅与上下文关键字通常的工作方式不一致,而且会使读者更难区分 yield-as-a-keyword 和 yield-as-an-identifier,这也会使其更难实现:

您不仅无法仅通过查看该行来判断 yield(x); 是否是 yield 语句(您需要查看整个方法);解析器也不会 - 它必须知道该方法是否包含 return 语句。这将需要在语法中对带有和不带有 return 的主体进行两个不同的定义 - 以及在每个主体中允许作为标识符的单独定义。这将是一个可怕的语法,看起来和实施起来都很糟糕。

在实践中,人们很可能会创建一个模棱两可的语法,然后将 yield (x); 解析为占位符 AST,其中包含它是 yield 语句或函数调用的可能性。然后你会尝试对两者进行类型检查并丢弃不进行类型检查的那个。这会奏效,但这种做法很少见,并且需要对编译器中的解析工作方式以及它随后与 AST 的工作方式进行大量更改。该语言的任何其他实现(Mono、Roslyn)也将不得不处理这种复杂性,这使得创建新实现变得更加困难。

结论

总而言之,解决此问题的两种方法都会导致一些不一致,而且后者也很难实施。只有在与 return 一起使用时才将 yield 视为特殊的,避免了歧义,而不会产生任何不一致,并且易于实现。

仔细想想,我不认为让 yield 成为上下文关键字,即使后面没有跟 return,也会破坏向后兼容性。然而,这会导致一些奇怪的事情。

例如

public class yield
{

}
...
public IEnumerable<yield> GetInts()
{
    yield item;
    item = new yield(); //compiler complains - item not declared.

    yield item2 = new yield(); //compiler complains-item2 not declared.
}

public int yield(int x) => 5;
public IEnumerable<int> GetInts()
{
    yield (5); //does not call the yield method as expected, but yields 5 instead
}

因此,尽管 none 这些方法无论如何都会编译 C# 2.0 之前的版本,但使用 yield return 避免了所有这些问题。