在我发现之前不久,新的dynamic
关键字与C#的foreach
语句不兼容:
using System;
sealed class Foo {
public struct FooEnumerator {
int value;
public bool MoveNext() { return true; }
public int Current { get { return value++; } }
}
public FooEnumerator GetEnumerator() {
return new FooEnumerator();
}
static void Main() {
foreach (int x in new Foo()) {
Console.WriteLine(x);
if (x >= 100) break;
}
foreach (int x in (dynamic)new Foo()) { // :)
Console.WriteLine(x);
if (x >= 100) break;
}
}
}
我期望迭代dynamic
变量应该完全正常,就像在编译时已知集合变量的类型一样。我发现第二个循环实际上在编译时看起来像这样:
foreach (object x in (IEnumerable) /* dynamic cast */ (object) new Foo()) {
...
}
并且每次访问x变量都会产生动态查找/强制转换,因此C#忽略了我在foreach语句中指定了正确的x
类型 - 这对我来说有点令人惊讶......而且,C#编译器完全忽略了动态类型变量的集合可能会实现IEnumerable<T>
接口!
C#4.0规范 8.8.4 foreach声明文章中描述了完整的foreach
语句行为。
但是......完全可以在运行时实现相同的行为!可以添加一个额外的CSharpBinderFlags.ForEachCast
标志,将已发布的代码更正为:
foreach (int x in (IEnumerable<int>) /* dynamic cast with the CSharpBinderFlags.ForEachCast flag */ (object) new Foo()) {
...
}
并为CSharpConvertBinder
添加一些额外的逻辑:
IEnumerable
个收藏集和IEnumerator
换成IEnumerable<T>
/ IEnumerator<T>
。Ienumerable<T>
/ IEnumerator<T>
来实现此接口。所以今天foreach
语句迭代dynamic
完全不同于迭代静态已知的集合变量,并完全忽略用户指定的类型信息。所有这些都会导致不同的迭代行为(IEnumarble<T>
- 实现集合正在迭代为IEnumerable
- 实现),并且在迭代150x
时会慢于dynamic
。简单的修复将带来更好的性能:
foreach (int x in (IEnumerable<int>) dynamicVariable) {
但为什么我应该写这样的代码?
很高兴看到有时C#4.0 dynamic
的工作方式完全相同,如果在编译时知道类型,但很遗憾看到dynamic
在IT CAN可以完全不同的地方工作与静态类型代码的工作方式相同。
所以我的问题是:为什么foreach
超过dynamic
与foreach
的工作方式不同?
答案 0 :(得分:23)
首先,向那些对这个问题感到困惑的读者解释一些背景:C#语言实际上不需要收集“foreach”工具IEnumerable
。相反,它需要它实现IEnumerable
,或者它实现IEnumerable<T>
,或者只是它具有GetEnumerator方法(并且GetEnumerator方法返回具有Current的内容)和MoveNext匹配预期的模式,依此类推。)
对于像C#这样的静态类型语言来说,这似乎是一个奇怪的功能。我们为什么要“匹配模式”?为什么不要求集合实现IEnumerable?
在仿制药之前考虑世界。如果你想创建一个int集合,你必须使用IEnumerable。因此,每次调用Current都会输入一个int,然后当然调用者会立即将其解包回int。这很慢并且会给GC带来压力。通过使用基于模式的方法,您可以在C#1.0中创建强类型集合!
现在当然没有人实现这种模式;如果你想要一个强类型的集合,你实现IEnumerable<T>
就完成了。如果C#1.0可以使用泛型类型系统,那么首先应该实现“匹配模式”功能。
正如您所指出的那样,为foreach 中的动态集合生成的代码不是查找模式,而是查找到IEnumerable 的动态转换(然后从返回的对象进行转换)当然,当然还有循环变量的类型。)所以你的问题基本上是“为什么使用动态类型作为foreach的集合类型生成的代码无法在运行时查找模式?”
因为它不再是1999年,即使它回到了C#1.0天,使用该模式的集合也几乎总是实现IEnumerable。真正的用户将编写生产质量C#4.0代码的概率非常低,该代码在实现模式但不是IEnumerable的集合中执行foreach。现在,如果你处于那种情况,那么,这是出乎意料的,我很抱歉我们的设计无法预测你的需求。如果您认为您的方案实际上是常见的,并且我们错误地判断它是多么罕见,请发布有关您的方案的更多详细信息,我们会考虑将其更改为假设的未来版本。
请注意,我们为IEnumerable生成的转换是动态转换,而不仅仅是类型测试。这样,动态对象可以参与;如果它没有实现IEnumerable但希望提供一个代理对象,那么它是免费的。
简而言之,“动态foreach”的设计是“动态地向对象请求IEnumerable序列”,而不是“动态地执行我们在编译时所做的每个类型测试操作”。这在理论上巧妙地违反了动态分析给出与静态分析相同的结果的设计原则,但在实践中,我们期望绝大多数动态访问的集合能够工作。
答案 1 :(得分:2)
但为什么我应该写这样的代码?
事实上。为什么编译器会编写这样的代码?你已经删除了它可能不得不猜测循环可以被优化的任何机会。顺便说一句,你似乎错误地解释了IL,它是重新绑定以获得IEnumerable.Current,MoveNext()调用是直接的,而GetEnumerator()只被调用一次。我认为这是合适的,下一个元素可能会或可能不会毫无问题地转换为int。它可以是各种类型的集合,每个类型都有自己的粘合剂。