3

List<T>.InsertRange()当我注意到一个奇怪的特性时,我正在阅读 .NET 4.0 框架中的代码。这是供参考的代码:

    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++; 
    } 
}

IEnumerable它的唯一缺点是额外的局部变量(可能会被 JIT 到寄存器中),工作集中可能增加 20 个字节,以及插入s时额外的 CPU 工作量无关紧要。如果需要避免额外的 bool 或循环分配,则IEnumerable可以将 s 的插入执行为

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);
    }
}
4

3 回答 3

2

我猜这是故意的。C# 遵循“成功的坑”设计——他们希望不容易出错

最终,现有设计使分析该方法的使用变得更加容易。我的意思是什么?好:

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

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

随着它抛出任何成功的调用,该故障模式变得更容易检测到。

于 2012-06-14T05:06:32.617 回答
1

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

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

于 2012-06-14T04:45:21.840 回答
0

这里的想法是,您不应该在修改列表时编写从列表中读取的代码,即使您的调用实际上并未修改列表,也可能会出现不同的调用。每次都简单地使列表无效是最安全的,即使有时它可能效率较低。

于 2012-06-14T04:55:32.760 回答