当我注意到一个奇怪的特性时,我正在阅读.NET 4.0框架中List<T>.InsertRange()
的代码。以下是供参考的代码:
public void InsertRange(int index, IEnumerable<T> collection) {
if (collection==null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
}
if ((uint)index > (uint)_size) {
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_Index);
}
Contract.EndContractBlock();
ICollection<T> c = collection as ICollection<T>;
if( c != null ) { // if collection is ICollection<T>
int count = c.Count;
if (count > 0) {
EnsureCapacity(_size + count);
if (index < _size) {
Array.Copy(_items, index, _items, index + count, _size - index);
}
// If we're inserting a List into itself, we want to be able to deal with that.
if (this == c) {
// Copy first part of _items to insert location
Array.Copy(_items, 0, _items, index, index);
// Copy last part of _items back to inserted location
Array.Copy(_items, index+count, _items, index*2, _size-index);
}
else {
T[] itemsToInsert = new T[count];
c.CopyTo(itemsToInsert, 0);
itemsToInsert.CopyTo(_items, index);
}
_size += count;
}
}
else {
using(IEnumerator<T> en = collection.GetEnumerator()) {
while(en.MoveNext()) {
Insert(index++, en.Current);
}
}
}
_version++;
}
特别注意,_version总是在函数结束时递增。这意味着列表中正在进行的枚举将在对InsertRange的任何非异常调用时失效,即使列表未更改也不会。例如,以下代码抛出:
static void Main(string [] args) {
var list = new List<object>() {1, 2 };
using(var enumerator = list.GetEnumerator()) {
if(enumerator.MoveNext())
Console.WriteLine(enumerator.Current);
list.InsertRange(1, new object[]{});
if(enumerator.MoveNext()) // ** InvalidOperationException
Console.WriteLine(enumerator.Current);
}
}
修改方法以便枚举不会以这种方式失效根本不会增加执行时间,因为代码已经检查count
的大小。它可以改写如下:
public void InsertRange(int index, IEnumerable<T> collection) {
if (collection==null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.collection);
}
if ((uint)index > (uint)_size) {
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_Index);
}
Contract.EndContractBlock();
ICollection<T> c = collection as ICollection<T>;
if( c != null ) { // if collection is ICollection<T>
int count = c.Count;
if (count > 0) {
EnsureCapacity(_size + count);
if (index < _size) {
Array.Copy(_items, index, _items, index + count, _size - index);
}
// If we're inserting a List into itself, we want to be able to deal with that.
if (this == c) {
// Copy first part of _items to insert location
Array.Copy(_items, 0, _items, index, index);
// Copy last part of _items back to inserted location
Array.Copy(_items, index+count, _items, index*2, _size-index);
}
else {
T[] itemsToInsert = new T[count];
c.CopyTo(itemsToInsert, 0);
itemsToInsert.CopyTo(_items, index);
}
_size += count;
_version++;
}
}
else {
var inserted = false;
using(IEnumerator<T> en = collection.GetEnumerator()) {
while(en.MoveNext()) {
inserted = true;
Insert(index++, en.Current);
}
}
if (inserted) _version++;
}
}
唯一的缺点是额外的局部变量(可能被JIT到寄存器中),工作集可能增加20个字节,插入IEnumerable
时额外的CPU工作量无关紧要。如果需要避免额外的bool或环路分配,IEnumerable
的插入可以作为
if(en.MoveNext()) {
Insert(index++, en.Current);
_version++;
}
while(en.MoveNext()) {
Insert(index++, en.Current);
}
因此...
.NET实现是预期的行为,还是错误?
修改
我意识到如果你在一个线程上枚举而在另一个线程上修改线程,那么你做错了什么。根据文档,这些情况下的行为是不确定的。但是,List<T>
使程序员受宠,并在这些情况下抛出异常。我不是在问List<T>
是否正确遵循文档:它确实如此。我问它是否以一种不是微软的意图实现。
如果InsertRange()
的行为符合预期,则List<T>
行为不一致。如果实际删除了项目,RemoveRange()
方法只会使枚举失效:
static void Main(string [] args) {
var list = new List<object>() {1, 2 };
using(var enumerator = list.GetEnumerator()) {
if(enumerator.MoveNext())
Console.WriteLine(enumerator.Current);
list.RemoveRange(1, 0);
if(enumerator.MoveNext()) // ** Does not throw
Console.WriteLine(enumerator.Current);
}
}
答案 0 :(得分:2)
我猜这是故意的。 C#遵循“成功的坑”设计 - 他们希望 hard 犯错误。
最终,现有设计可以更轻松地分析该方法的使用。那是什么意思?良好:
你引用的例子是微不足道的,一眼就可以看出它并没有真正修改列表。但在几乎所有现实世界的代码中并非如此。插入的序列几乎肯定会被动态创建,并且可能几乎是随机的空序列。一个空序列真的应该有不同的表现吗?如果插入的序列是空的,那么你的代码就可以工作了,但是当你在那里放置一些真实的东西时,你的代码就可以了
想象一下,如果您第一次编写此代码并且所有序列都是空的;看起来很有效。然后,在某个任意时间之后,你有一个非空的插入。现在你得到例外。插入问题和检测问题之间的距离可能非常大。
随着它投入任何成功的呼叫,该故障模式变得更容易检测。
答案 1 :(得分:1)
这可能是有意的。调用函数插入项目时的想法是修改列表。列表最终未被修改的情况是一个例外,但目的是修改它。我猜这里的意图很重要。如果有人在同时迭代列表的同时调用InsertRange
,那么就已存在概念问题。
就我个人而言,我更喜欢Java集合框架 - ListIterator
类可以通过自己的Add
,Set
和Remove
方法修改列表。即使是通用Iterator
也可以从迭代集合中删除项目。但据我所知,Java还会将迭代器本身以外的任何修改尝试视为使(list)迭代器失效的操作。
答案 2 :(得分:0)
这里的想法是你不应该在修改代码时编写从列表中读取的代码,即使你的调用实际上没有修改列表,也可能是另一个调用。每次只是使列表无效是最安全的,即使偶尔它可能效率较低。