3

.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 が常にインクリメントされることに注意してください。これは、List が変更されていなくても、 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 バイトの増加、およびIEnumerables を挿入するときの余分な CPU 作業の無関係な量があります。余分な bool またはループ内代入を回避する必要がある場合、IEnumerables の挿入は次のように実行できます。

if(en.MoveNext()) {
    Insert(index++, en.Current); 
    _version++;
}

while(en.MoveNext()) {
    Insert(index++, en.Current); 
}  

そう...

.NET 実装は意図した動作ですか、それとも間違いですか?

編集:

あるスレッドで列挙しているときに別のスレッドでスレッドを変更している場合、何か間違ったことをしていることに気付きました。ドキュメントによると、これらの場合の動作は未定義です。ただし、List<T>プログラマーは好意的であり、これらの場合に例外をスローします。List<T>ドキュメントに正しく従っているかどうかを尋ねているのではありません。そうです。Microsoft が意図したものとは異なる方法で実装されているかどうかを尋ねています。

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,SetおよびRemoveメソッドを介してリストを変更できます。将軍でさえ、Iterator反復されたコレクションからアイテムを削除できます。しかし、私の知る限り、Java は (リスト) イテレーターを無効にする操作として、イテレーター自体からの変更以外の変更の試みも考慮します。

于 2012-06-14T04:45:21.840 に答える
0

ここでの考え方は、呼び出しが実際にリストを変更しない場合でも、変更中にリストから読み取るコードを記述すべきではないということです。たまに効率が悪い場合でも、毎回単純にリストを無効にするのが最も安全です。

于 2012-06-14T04:55:32.760 に答える