List<T> 实现了三个获取迭代器方法,一个是类自身的方法,两个是显示的接口实现:
public List<T>.Enumerator GetEnumerator()
IEnumerator<T> IEnumerable<T>.GetEnumerator()
IEnumeraotr IEnumerable.GetEnumerator()
这三个方法的内部实现都是一样的:
return new List<T>.Enumerator(this);
那么问题来了,当我们写下如下的 foreach#1时,发生了什么?
foreach#1
List<TNode> listthings;
foreach(var thing in listthings)
想要清楚的解释这个问题,就涉及到 foreach 的机制。当我们写下 foreach(E e in C)时,首先检查的是 C 是否有 GetEnumerator 方法,如果有,再检查 GetEnumerator 返回的类型是否实现了 MoveNext 方法和 Current 属性。如果有, foreach 将直接使用这些方法和属性。 如果前面检查的不成立,才会尝试将 C 转换成 IEnumerable。
然后我们会过来看上面的问题,List<T> 有 GetEnumerator 方法。那么它返回的 List<T>.Enumerator 是个什么东东呢?
public strcut Enumerator : IEnumerator<T>, IDisposable,
IEnumerator
首先它是个 struct,没错,value type。 当你使用 List<T>时,请时刻记住这一点。
public T Current {get;}
public bool MoveNext();
其次它符合了上面提到的检查第二点,所以 foreach愉快地采用了 listthings.GetEnumerator。
为什么 Enumerator 是 struct?
为了效率,当你用完 foreach,Enumerator 便可自动销毁,不用等到 GC。BCL 在做了大量研究之后采用了这一策略。
不过这一点既有好处,又有很容易就踩的坑。坑是什么,没错,就是装箱。
我们再来看看 foreach#2
foreach#2
var iter = listthings as IEnumerable;
foreach(var thingobj in iter)
var iter2 = listthings as IEnumerable<TNode>;
foreach(var thing in iter2)
将 List<T>.Enumerator 转为 IEnumeraotr 时,就完成了装箱。 当然平时我们不会直接做这个转换,但写如下的代码却是很 easy:
void PrintAddress(IEnumerable<Address> addresslist)
{
foreach(var adr in addresslist)
}
PrintAddress(List<Address>)
如此很难直觉的看出有装箱问题。又比如写起来很顺手的 Linq: listOfAddress.First() 等等。
所以在性能要求高的地方,应避免使用 IList,Linq. 直接使用 List,避免 GC 带来的压力。
关于List<T>.Enumerator的实现细节,还有很多有意思的地方。请参考链接:
http://marcinjuraszek.com/2013/10/playing-around-with-listt-part-two-ienumerable-and-ienumerablet-implementation.html