.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 バイトの増加、およびIEnumerable
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>
ドキュメントに正しく従っているかどうかを尋ねているのではありません。そうです。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);
}
}