正如他们所说,魔鬼在细节中……
两种收集枚举方法之间的最大区别是foreach
携带状态,而不携带状态ForEach(x => { })
。
但是,让我们再深入一点,因为您应该意识到某些事情会影响您的决策,并且在为每种情况编码时都需要注意一些警告。
让我们List<T>
在我们的小实验中观察行为。对于此实验,我正在使用.NET 4.7.2:
var names = new List<string>
{
"Henry",
"Shirley",
"Ann",
"Peter",
"Nancy"
};
让我们首先迭代一下foreach
:
foreach (var name in names)
{
Console.WriteLine(name);
}
我们可以将其扩展为:
using (var enumerator = names.GetEnumerator())
{
}
手持枚举器,在幕后我们可以看到:
public List<T>.Enumerator GetEnumerator()
{
return new List<T>.Enumerator(this);
}
internal Enumerator(List<T> list)
{
this.list = list;
this.index = 0;
this.version = list._version;
this.current = default (T);
}
public bool MoveNext()
{
List<T> list = this.list;
if (this.version != list._version || (uint) this.index >= (uint) list._size)
return this.MoveNextRare();
this.current = list._items[this.index];
++this.index;
return true;
}
object IEnumerator.Current
{
{
if (this.index == 0 || this.index == this.list._size + 1)
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
return (object) this.Current;
}
}
两件事立即显现:
- 我们返回了一个对底层集合有深入了解的有状态对象。
- 集合的副本是浅表副本。
当然,这绝对不是线程安全的。正如上面所指出的,在迭代时更改集合只是不好的尝试。
但是,在迭代过程中由于我们在迭代过程中厌恶集合而使集合变得无效的问题又如何呢?最佳实践建议在操作和迭代期间对集合进行版本控制,并检查版本以检测基础集合何时更改。
这是事情变得很模糊的地方。根据Microsoft文档:
如果对集合进行了更改(例如添加,修改或删除元素),则枚举器的行为是不确定的。
好吧,那是什么意思?举例来说,仅仅因为List<T>
实现异常处理并不意味着所有实现的集合都IList<T>
将做同样的事情。这似乎明显违反了《里斯科夫换人原则》:
超类的对象应可被其子类的对象替换,而不会破坏应用程序。
另一个问题是枚举器必须实现 IDisposable
-这意味着潜在的内存泄漏的另一个来源,不仅是调用方弄错了,而且如果作者没有实现Dispose
正确模式。
最后,我们遇到了生命周期问题……如果迭代器有效,但是基础集合消失了,会发生什么呢?我们现在的快照一下 ……当您分离集合及其迭代器的生命周期时,您正在自找麻烦。
现在让我们检查一下ForEach(x => { })
:
names.ForEach(name =>
{
});
扩展为:
public void ForEach(Action<T> action)
{
if (action == null)
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
int version = this._version;
for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
action(this._items[index]);
if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
return;
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}
重要的注意事项如下:
for (int index = 0; index < this._size && ... ; ++index)
action(this._items[index]);
此代码未分配任何枚举数(对Dispose
无效),并且不会暂停在迭代时。
请注意,这还会执行基础集合的浅表副本,但是该集合现在是及时的快照。如果作者未正确执行对集合更改或过时的检查,则快照仍然有效。
这丝毫不会保护您免受生命周期问题的困扰……如果基础集合消失了,您现在将获得一个浅表副本,该副本指向以前的内容……但至少您没有 Dispose
问题处理孤立的迭代器...
是的,我说过迭代器...有时候拥有状态是有利的。假设您想要维护类似于数据库游标的东西……也许要使用多种foreach
样式Iterator<T>
。我个人不喜欢这种设计风格,因为存在太多生命周期问题,并且您依赖于所依赖的集合的作者的良好风度(除非您从头开始编写所有内容)。
总有第三个选择...
for (var i = 0; i < names.Count; i++)
{
Console.WriteLine(names[i]);
}
它不是性感的,但它有牙齿(对汤姆·克鲁斯和电影《公司》的道歉)
它是您的选择,但现在您知道了,它可以成为有见识的人。