C语言基础C#之11:PreferforeachLoops
# C的foreach表达式不仅仅是do、while、for循环的变体。它为您拥有的任何集合生成的迭代代码。它的定义绑定到。Net framework,以及C#编译器为这种特定类型的集合生成的代码。迭代集合时,使用foreach而不是其他循环结构。看看这些循环:
int[]foo = new int[100];
//循环1:
foreach(int I in foo)
Console。WriteLine(I . ToString());
//循环2:
for(int index = 0;index < foo。长度;index++)
控制台。WriteLine(foo[index].ToString());
//循环3:
int len = foo。长度;
for(int index = 0;index < lenindex++)
控制台。WriteLine(foo[index].ToString());
对于当前和未来的c#编译器(1.1版或更高版本),Loop 1没错,它甚至拥有最少的打字量,你的个人生产力可以得到提升。(C#1.0的编译器为Loop1生成较慢的代码,所以Loop2是那个版本)。3.大多数C和C++程序员认为是高效结构的Loop是最差的选择。通过将length变量提升到循环之外,您进行了以下更改:它阻碍了JIT编译器移除循环内部的检测范围。
C#代码运行在安全的托管环境中。检查每个内存分配,包括数组的索引。用一些库,Loop 3的实际代码有点像下面这样:
/loop3,由编译器生成:
int len = foo . length;
for(int index = 0;index < lenindex++)
{
if(index < foo。Length)
控制台。WriteLine(foo[index].ToString());
else
throw new IndexOutOfRangeException();
}
JITC#编译器就是不喜欢你这样帮它。您试图将对length属性的访问提升到循环之外只会让JIT编译器做更多的工作,甚至生成更慢的代码。CLR的保证之一是你不能在你的变量内存之外写代码。在访问每个特定的数组元素之前,运行时将为实际的数组边界生成一个测试(不是您的len变量)。你以两倍的代价得到了边界检查。
Examda提示:每次循环重复的时候都需要支付数组的索引检查,你已经做了两次了。循环1和循环2之所以更快,是因为C#编译器和JIT编译器可以验证循环的边界,保证其安全性。任何时候,当循环变量不是数组的长度时,边界检查将在每次重复时执行。
在原C#编译器下,foreach和array生成非常慢的代码的原因是装箱,在第17项会被覆盖。该数组是类型安全的。现在,foreach为来自其他集合的数组生成不同的IL。该版本数组不使用IEnumerator接口,需要装箱和取消装箱操作:
IEnumerator it = foo . get enumerator();
while(它。MoveNext())
{
int I =(int)it。当前;//在这里装箱和拆箱。
控制台。WriteLine(I . ToString());
}
相反,foreach表达式为数组生成这样的结构:
for(int index = 0;index < foo。长度;index++)
控制台。WriteLine(foo[index].ToString());
foreach总是生成代码。您不必记住哪个构造会生成有效的循环结构:foreach和编译器会为您完成这项工作。
如果效率对你来说还不够,那么考虑语言互操作性。世界上有些家伙(是的,他们中的大多数使用其他编程语言)强烈认为索引变量以1开始,而不是0。无论我们如何努力,我们都无法改变他们的习惯。的。网队以前试过。您必须用C#编写这样的初始化,以便获得一个以非0开头的数组。
//创建一维数组。
//它的范围是[ 1..5 ]
数组测试=数组。CreateInstance(typeof(int),new int[] { 5 },new int[]{ 1 });
这段代码足以让任何人畏缩,只写从0开始的数组。但是有些人很固执。尽力而为,他们会从1开始数。幸运的是,这是您可以强加给编译器的许多问题之一。使用foreach迭代测试数组:
foreach(int j in test)
console . writeline(j);
Foreach表达式知道如何检查一个数组的上下界,所以你不必去做——而且和手工编码一样快,不管有些人决定用什么不同的下界。
foreach为您增加了其他语言的好处。变量是只读的:使用foreach时,不能替换集合中的对象。同时,有一个到正确类型的直接转换。如果集合包含错误类型的对象,迭代将引发异常。
Examda提示:foreach为多维数组提供了类似的好处。假设您正在创建一个棋盘。你可以这样写下2个片段:
私人广场[,] Theboard =新广场[8,8];
//代码中的其他位置:
for(int I = 0;我< theBoard。GetLength(0);i++)
for(int j = 0;j < theBoard。GetLength(1);j++ )
theBoard[ i,j ]。paint square();
相反,你可以这样简化画棋盘:
foreach(棋盘中的square sq)
sq . paint square();
foreach表达式生成适当的代码来迭代此数组的所有维度。如果以后做3D棋盘,foreach还是可以的。其他需要修改的循环:
for(int I = 0;我< theBoard。GetLength(0);i++)
for(int j = 0;j < theBoard。GetLength(1);j++)
for(int k = 0;k < theBoard。GetLength(2);k++ )
theBoard[ i,j,k ]。paint square();
事实上,在多维数组上,即使每个维度有不同的下界,foreach也可以工作。我不想写那种代码,甚至不想写一个例子。但是别人那样写汇编代码,foreach都能应付。
如果您后来发现需要从数组的较低层次修改数据结构,foreach还为您提供了保持大量代码不变的灵活性。我们用一个简单的数组来讨论一下:
int[]foo = new int[100];
假设,在未来的某个时刻,你意识到你需要数组类不那么容易处理的能力。可以简单的将数组修改为ArrayList:
/设置初始大小:
ArrayList foo = new ArrayList(100);
同时,任何对循环的手工编码都将被销毁:
int sum = 0;
//不会编译:ArrayList使用Count而不是Length
for(int index = 0;index < foo。长度;index++)
//不会编译:foo[ index ]是object,不是int。
sum+= foo[index];
然而,foreach循环被编译成不同的代码:它自动将每个操作数转换成适当的类型。不需要做任何转换。转换不仅仅是标准化集合类,或者使任何集合类型都可用于foreach。
如果您支持。Net环境中,您的类型的用户可以使用foreach迭代所有成员。对于foreach表达式,将其视为一个集合类型,一个必须至少有一个属性的类。公公的GetEnumerator()方法的存在,形成了一个集合类。直接实现IEnumerable接口来创建集合类型。实现IEnumerator接口以创建集合类型。Foreach和其中任何一个都可以一起使用。
foreach还有一个好处就是忽略了资源管理。IEnumerable接口包含一个方法:GetEnumerator()。下面的代码是在可枚举类型上使用foreach表达式生成的,同时对其进行了优化:
ienumerator it = foo . get enumerator()as ienumerator;
using(IDisposable disp = it as IDisposable)
{
while(it。MoveNext())
{
int elem =(int)it。当前;
sum+= elem;
}
}
如果编译器可以确定这个枚举器是否实现了IDisposable,它将自动优化最后括号中的代码。但是让你看到这个非常重要。无论发生什么,foreach都会生成正确的代码。
foreach是一个非常通用的表达式。它为数组的上限和下限生成正确的代码,迭代多维数组,强制将操作数转换为正确的类型(使用最有效的构造),最重要的是,它具有高效的循环结构。这是一种对集合进行迭代的方法。有了它,您可以创建更持久的代码,并且很容易在它第一次出现的地方编写它。使用它是生产力的一个小增长,但它的效果会随着时间的推移而增加。
0条评论