for 与foreach 的区别

来源:互联网 发布:在迪拜学英语 知乎 编辑:程序博客网 时间:2024/05/16 15:49

for和foreach是不一样的(不仅仅是语法),在网上也看到很很多说明的文章。
但从自己写的代码中来看,很难看出区别在那,因为大多数时候,都是用for或者foreach
对一个数组结构的类进行遍历操作。

某天突然想弄清楚这个问题,于是小小的分析了一下,看看下段代码:

public void For()
{
 string[] array = new string[]{"111","222","333"};

 for(int i = 0; i < array.Length; i++)
 {
  Console.WriteLine(array[i]);
 }
}
////Results :
111
222
333


public void ForeachOnArray()
{
 string[] array = new string[]{"111","222","333"};

 foreach(string s in array)
 {
  Console.WriteLine(s);
 }
}
////Results :
111
222
333

 

奇怪,虽然所调用的指令不不同,但也是差不多,都是根据length来对数据进行循环。(与网上说得有点不一样哦)
(在这里主要不同的是foreach里自动对array进行index + 1的操作来循环,而for则是自己的代码控制的。)

再加点代码,在循环中试图更改所操作的值:
array = new string[]{"AAA","BBB","CCC"};

///For() result:
111
BBB
CCC

///ForeachOnArray() result:
111
222
333

不一样了把,看来在foreach内部的循环中对源的更改不是即使生效的!

如果试着更改当前操作的数组内的值:
For() :
array[i] = "changed"; //OK

ForeachOnArray() :
s = "Changed";  //编译时Error,提示: “Cannot assign to 's' because it is read-only”

如果改为:
ForeachOnArray() :
array[2] = "Changed"; //在Foreach()内部无效,跳出Foreach()循环时更改才生效。


恩,以上的区别是很显而易见了,
结论:在Foreach(...)循环里尽量不要更改操作的源,
在For(...)循环里则无所谓(看起来For跟do.While.的循环更类似,核心仅仅是判断)

但这就是全部的真实了吗?

NO,来考虑下我们经常在DataRowCollection( 即 DataTable.DataRow )上做的循环--它可不是一个数组,
而根据MS的参考,能在foreach上做循环的只能是实现了IEnumerable接口的类. (事实上,System.Array也是实现了IEnumerable接口的)

恩,现在抛开数组,来做一个在IEnumerable上的循环,先编写如下的实现了IEnumberable接口的类E:

 public class E : IEnumerable
 {
  private InnerEnumerator inner;

  public E(string[] array)
  {
   this.inner = new InnerEnumerator(array);
  }

  #region IEnumerable Members

  public IEnumerator GetEnumerator()
  {
   return this.inner;
  }

  #endregion

  private class InnerEnumerator : IEnumerator, IDisposable
  {
   private string[] s;
   private int currentIndex;

   public InnerEnumerator(string[] array)
   {
    this.s = array;
    this.Reset();
   }

   #region IEnumerator Members

   //Reset index to original
   public void Reset()
   {
    this.currentIndex = s.Length - 1;
   }

   //Get Current object inner
   public object Current
   {
    get
    {
     object o = this.s[this.currentIndex];
     this.currentIndex--;
     return o;
    }
   }

   //Is there has any other object in the array?
   public bool MoveNext()
   {
    if(this.currentIndex < 0)
    {
     return false;
    }
    return true;
   }

   #endregion

   #region IDisposable Members

   //Dispose Here
   public void Dispose()
   {
    Console.WriteLine("Dispose here !");
   }

   #endregion
  }
 }

接下来,拿这个类做测试,在上面做循环看看:(过程与For()和ForeachOnArray()大致一样)

public void ForeachOnIEnumerable()
{
 string[] array = new string[]{"111","222","333"};
 E e = new E(array);

 foreach(string s in e)
 {
  Console.WriteLine(s);
 }
}

//result:
333
222
111
Dispose here !

差异出现了,这次是按照倒序的方式,而且看样子还是自动调用了Dispose方法!
(NOTE : 我们可没有自己编写调用Dispose()方法的代码!)

还是老规矩,用ILdasm看看实际执行的IL代码:

果然,多了很多不一样的东东 
try
{
}
finally
{
}

分析:
顺序:
1.调用e.GetEnumerator()方法,获取一个Enumerator实例
 等同于代码:
  IEnumerator ienumerator = e.GetEnumertor();

2.在这个Enumerator实例上调用GetCurrent()方法,将获取到的object 通过 castclass 指命
  转换为我们在代码中定义的类型string
 即等同与代码: 
  string s = (string)ienumerator.GetCurrent();
 (NOTE : 如果失败,会抛出InvalidCastException异常,表明转换失败,类型不匹配)
 (即:如果我们这里改为:foreach(int s in array),就会发生运行时的错误,抛出InvalidCastException异常,
 这就是castclass关键字的作用)

3.对这个GetCurrent()所得的object做自己想要得处理
 在这里就是我们自己编写的代码:
  Console.WriteLine(s);

4.在获取得IEnumerator上调用MoveNext(),如果true,则进入下次循环(跳转到2),如果为false,则跳出循环
 等同于代码:
  if(ienumerator.MoveNext())
  {
   goto step-2;
  }
  else
  {
   goto finally;
  }

finally块代码:
1.判断前面获取的ienumerator是否实现了IDisposable接口,true则继续,false则break;
 等同样代码:
  if(!(ienumerator is IDispose))
  {
   break; //over whole
  }
2.调用IDisposable上的Dispose()方法(这就是为什么输入最后有一句"Dispose here !"了)
 因为我们写在Dispose()中的方法在这里被自动调用了!!!
 等同于代码:
  ienumerator.Dispose()

分析:
。。。
这与前面在Array上做的循环(ForeachOnArray())可大不一样,是两段完全不同的代码!


综合上面,得出如下结论:
1.for循环并不依赖于数组或其他形式的组式数据结构,只是简单的
 在调用了代码后,进行一个判断,判断是否要继续。
 (非常类似于do..while和while循环--在这里不作具体分析了^_^~~)
2.foreach循环如果作用在一个基于System.Array的类型之上的数组的话,编译器会自动优化成与for循环非常类似
 的代码,只是调用的指命有细微的差别,并且检查(包括编译阶段和运行时)会比for严格的多
3.foreach循环作用在一个非System.Array类型上(且一定要是实现了IEnumerable接口的类),会先调用
 IEnumerable.GetEnumerator()方法获取一个Enumertor实例,再在获取的Enumertor实例上调用
 GetCurrent()和MoveNext()方法,最后判断如果Enumertor实例如果实现了IDispose接口,就自动调用
 IDispose.Dispose()方法!

那么我们应该分别在那些地方用for和foreach捏
建议:
1.在有对所循环的本体(System.Array)做赋值操作时,尽量不要用Foreach()。
2.foreach比for更灵活。(可在MoveNext()和GetCurrent()里编写自己的代码).
 自己编写的类如果实现了IEnumerable接口的话,就可以用foreach循环了,而不管内部是否有一个真实的数组,
 并且可以自定义循环的规则。
3.从OO的原则看,foreach循环更适于多数情况的使用
 (事实上,foreach的实现是典型的Iterator模式,下面有简单的描述它的好处)
 想用统一的调用循环接口时,foreach是最佳的选择
 (MS有很多类就是这样的,例如前面提到的DataRowCollection.)
 

 

补充说明:
/////////////////////////////////////////////////////////////////////////////////////////////
isinit:

如果再做进一步的试验的话,可以发现,其实这里的isinit就是C#的is关键字(而as关键字的核心也是isinit这个操作码)
获取反过来说,is,as关键字在IL代码里表现为这个isinit.
isinst不会触发异常,只是判断类型是否兼容。

/////////////////////////////////////////////////////////////////////////////////////////////
castclass:

castclass用于强制转换:
castclass指命会引发如下一样:
InvalidCastException - Specified cast in not valid

而如果Foreach的作用在一个string[]类型的数组( 例如上面的代码改为 : foreach(int s in array) )
这种错误在编译时就能检测出来,提示:Cannot convert type 'string' to 'int'

/////////////////////////////////////////////////////////////////////////////////////////////
关于Iterator模式:

Iterator模式是用于遍历集合类的标准访问方法。它可以把访问逻辑从不同类型的集合类中抽象出来,
从而避免向客户端暴露集合的内部结构。

特点:[引用]
"要确保遍历过程顺利完成,必须保证遍历过程中不更改集合的内容,
因此,确保遍历可靠的原则是只在一个线程中使用这个集合,或者在多线程中对遍历代码进行同步。"
这也能解释为什么C#.NET在Foreach上要做严格的使用限制,而For则没有。

0 0
原创粉丝点击