List <t> .InsertRange()中的错误,还是设计决策?</t>

时间:2012-06-14 04:36:47

标签: c# .net list frameworks implementation

当我注意到一个奇怪的特性时,我正在阅读.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);
    }
}

3 个答案:

答案 0 :(得分:2)

我猜这是故意的。 C#遵循“成功的坑”设计 - 他们希望 hard 犯错误。

最终,现有设计可以更轻松地分析该方法的使用。那是什么意思?良好:

你引用的例子是微不足道的,一眼就可以看出它并没有真正修改列表。但在几乎所有现实世界的代码中并非如此。插入的序列几乎肯定会被动态创建,并且可能几乎是随机的空序列。一个空序列真的应该有不同的表现吗?如果插入的序列是空的,那么你的代码就可以工作了,但是当你在那里放置一些真实的东西时,你的代码就可以了

想象一下,如果您第一次编写此代码并且所有序列都是空的;看起来很有效。然后,在某个任意时间之后,你有一个非空的插入。现在你得到例外。插入问题和检测问题之间的距离可能非常大。

随着它投入任何成功的呼叫,该故障模式变得更容易检测。

答案 1 :(得分:1)

这可能是有意的。调用函数插入项目时的想法是修改列表。列表最终未被修改的情况是一个例外,但目的是修改它。我猜这里的意图很重要。如果有人在同时迭代列表的同时调用InsertRange,那么就已存在概念问题。

就我个人而言,我更喜欢Java集合框架 - ListIterator类可以通过自己的AddSetRemove方法修改列表。即使是通用Iterator也可以从迭代集合中删除项目。但据我所知,Java还会将迭代器本身以外的任何修改尝试视为使(list)迭代器失效的操作。

答案 2 :(得分:0)

这里的想法是你不应该在修改代码时编写从列表中读取的代码,即使你的调用实际上没有修改列表,也可能是另一个调用。每次只是使列表无效是最安全的,即使偶尔它可能效率较低。