嵌套泛型语法歧义

12

显然,C# 与 C++ 一样容易受到 '>>' 词法分析问题的困扰

这段 C# 代码是有效的,可以编译和运行良好:

var List = new Dummy("List");
var Nullable = new Dummy("Nullable");
var Guid = new Dummy("Guid");

var x = List<Nullable<Guid>> 10;
var y =  List<Nullable<Guid>> .Equals(10,20);

要对上面的Dummy类重载'<'和'>>'运算符。

但编译器能够猜测在'x'情况下使用List、Nullable和Guid局部变量的意思。而在'y'情况下,它突然决定将它们视为众所周知的类型名称。

这里有一个更详细的描述和另一个例子: http://mihailik.blogspot.co.uk/2012/05/nested-generics-c-can-be-stinky.html

问题是:C#编译器如何将'a<b<c>>'解析为算术表达式或泛型类型/方法?

它肯定不会尝试多次遍历程序文本直到成功,难道会吗?这将需要无限的前瞻,并且是一个非常复杂的工具。


1
"在一个含义与另一个含义之间做出选择?" ... 你是什么意思?这里只有一个明确的含义...唯一的不清楚是由于变量名称的选择所导致的。 - Reed Copsey
'a<b<c>>' 的含义非常依赖于上下文。编程语言允许这种情况并不常见。实际上,主要问题是编译器如何决定它是算术移位还是通用类型规范的一部分。 - Oleg Mihailik
1
如果你疯狂到重载'<'和'>>',那么你应该预料会得到什么。 - Eric Dahlvang
1
这让我想起了 --><-- 运算符 ;) - Thomas Levesque
4
不是我疯狂地使用语言的合法特性,而更多关于编译器如何解决这个谜题。你和我看语法并应用常识,但编译器需要清晰的规则,它没有常识。 - Oleg Mihailik
显示剩余6条评论
2个回答

7
我被指向C#语言规范中的第7.6.4.2段:

http://download.microsoft.com/download/0/B/D/0BDA894F-2CCD-4C2C-B5A7-4EB1171962E5/CSharp%20Language%20Specification.htm

简单名称(§7.6.2)和成员访问(§7.6.4)的生成可能导致表达式语法上的歧义。

...

如果令牌序列可以解析为以类型参数列表(§4.4.1)结束的简单名称(§7.6.2)、成员访问(§7.6.4)或指针成员访问(§18.5.2),则检查跟在“>”符号后面的令牌。如果它是以下之一

( ) ] } : ; , . ? == != | ^

则将类型参数列表作为简单名称、成员访问或指针成员访问的一部分保留,而且任何其他可能的令牌解析都将被丢弃。否则,即使没有其他可能的令牌解析,类型参数列表也不被视为简单名称、成员访问或指针成员访问的一部分。请注意,在解析命名空间或类型名称(§3.8)中的类型参数列表时不应用这些规则。

因此,在涉及类型参数列表时确实可能会出现歧义,而且他们有一种廉价的方法来解决它,即向前查看一个令牌。

这仍然是一个未绑定的向前查看,因为在“>>”和下一个令牌之间可能有一兆字节的注释,但至少规则更或多或少地清晰。最重要的是,没有必要进行深度推测解析。


-2

编辑:我坚持认为没有歧义: 在你的例子中,根本没有歧义。这永远不可能被评估为List<Guid?>。上下文(额外的10)向编译器展示如何解释它。

var x = List<Nullable<Guid>> 10;

编译器能够编译这个吗?

var x = List<Guid?> 10;

很明显它不会。所以我仍在寻找歧义。

另一方面,第二个表达式:

var y =  List<Nullable<Guid>> .Equals(10,20);

必须将其评估为List<Guid?>,因为您正在调用.Equals方法。再次强调,这可以以任何其他方式解释。

根本没有悖论。编译器完美地解析了它。我仍然想知道哪里出现了悖论。

你犯了一个大错误。编译器解释整个表达式,并使用语言语法来理解它们。它不会像你所做的那样只看代码片段,而不考虑表达式的其余部分。

这些表达式按照C#语法进行解析。语法足够清晰,能够正确解释代码。即在

var x = List<Nullable<Guid>> 10;

很明显,10是一个字面量。如果你跟进语法,你会发现:10是一个*字面量,所以它是*非数组创建表达式,这是一个*主表达式,这是一个*一元表达式,这是一个*乘法表达式,这是一个*加法表达式。如果你在*>>的右侧寻找加法表达式,你会发现它必须是一个*移位表达式,因此*>>的左侧必须被解释为一个*加法表达式,依此类推。

如果你能找到一种不同的语法使用方式,并得到相同表达式的不同结果,那么我将同意你的观点,但让我不同意!

最后:

  • 对人类来说非常令人困惑
  • 对编译器来说绝对清晰明了

因为:

  • 我们人类通过识别对我们熟悉的文本片段来识别模式,比如 List<Nullable<Guid>>,并按照我们想要的方式进行解释。
  • 编译器不像我们一样解释代码,它们会将整个表达式与语言语法进行匹配,而不是只识别熟悉的片段,比如 List<Nullable<Guid>>

我说它编译通过是因为它确实可以。这段代码,我在Visual Studio 2010中编译并运行过。我知道这听起来不太可能,这就是为什么这是一件令人不安的事情。Dummy类在那个链接中,这里为了简洁起见我跳过了它。 - Oleg Mihailik
我尝试了这段代码,var x = List<Nullable<Guid>>(10); 出现错误“结构名称在此处无效”,指的是 Nullable。我真的很想看到它能够工作,但我做不到。你能展示整个代码,包括 "using" 部分吗?我在你的博客中也没有看到它。 - JotaBe
1
没有问题,如果我们没有犯错,我们就不需要 StackOverflow :-) - Oleg Mihailik
你通过对不同解释下表达式的试错解析来推断其含义。这在人类语言中很正常,因为歧义是代码的一部分。编译器不能来回尝试重新解释事物。它们之所以不能这样做,不是因为这是不可能的,而是因为它不可扩展。解析是从左到右进行的,只有非常有限的回退(或者说前瞻)重新解析是实际上可行的。 - Oleg Mihailik
你确定C#编译器没有回溯能力吗?我没有找到它的实现,但我敢说它可以回溯。当从左到右解析时,它可以找到两条可能的路径。如果到达路径的末尾并且找到的代码不正确,它可以返回并尝试另一条路径。你必须考虑到今天的计算机有足够的能力来做到这一点。它甚至可以同时走两条路径,直到找到正确的路径。继续... - JotaBe
显示剩余6条评论

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