{C#} 关于List的迭代器需要知道的一些事

List 实现了三个获取迭代器方法,一个是类自身的方法,两个是显示的接口实现:

public List.Enumerator GetEnumerator()
IEnumerator IEnumerable.GetEnumerator()
IEnumeraotr IEnumerable.GetEnumerator()

这三个方法的内部实现都是一样的:

return new List.Enumerator(this);

那么问题来了,当我们写下如下的 foreach#1时,发生了什么?

foreach#1 
List listthings;
foreach(var thing in listthings)

想要清楚的解释这个问题,就涉及到 foreach 的机制。当我们写下 foreach(E e in C)时,首先检查的是 C 是否有 GetEnumerator 方法,如果有,再检查 GetEnumerator 返回的类型是否实现了 MoveNext 方法和 Current 属性。如果有, foreach 将直接使用这些方法和属性。 如果前面检查的不成立,才会尝试将 C 转换成 IEnumerable。
然后我们会过来看上面的问题,List 有 GetEnumerator 方法。那么它返回的 List.Enumerator 是个什么东东呢?

public strcut Enumerator : IEnumerator, IDisposable, 

IEnumerator
首先它是个 struct,没错,value type。 当你使用 List时,请时刻记住这一点。

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;
foreach(var thing in iter2)

将 List.Enumerator 转为 IEnumeraotr 时,就完成了装箱。 当然平时我们不会直接做这个转换,但写如下的代码却是很 easy:

void PrintAddress(IEnumerable
addresslist) { foreach(var adr in addresslist) } PrintAddress(List
)

如此很难直觉的看出有装箱问题。又比如写起来很顺手的 Linq: listOfAddress.First() 等等。

所以在性能要求高的地方,应避免使用 IList,Linq. 直接使用 List,避免 GC 带来的压力。

关于List.Enumerator的实现细节,还有很多有意思的地方。请参考链接:
http://marcinjuraszek.com/2013/10/playing-around-with-listt-part-two-ienumerable-and-ienumerablet-implementation.html

你可能感兴趣的:({C#} 关于List的迭代器需要知道的一些事)