为什么我不能使用索引器将项添加到通用列表?

8

我今天遇到了一个奇怪的情况:

我有一个通用列表,我想使用它的索引器向列表中添加项,就像这样:

List<string> myList = new List<string>(10);
myList[0] = "bla bla bla...";

当我尝试这样做时,我得到了“ArgumentOutOfRangeException”的错误。
然后我查看了List索引器设置方法,它是这样的:
[TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries"),    __DynamicallyInvokable] 
  set
  {
    if ((uint) index >= (uint) this._size)  
      ThrowHelper.ThrowArgumentOutOfRangeException();  //here is exception
    this._items[index] = value;
    ++this._version;
  }

同时也看一下Add方法:

[__DynamicallyInvokable]
public void Add(T item)
{
  if (this._size == this._items.Length)
    this.EnsureCapacity(this._size + 1);
  this._items[this._size++] = item;
  ++this._version;
}

现在,我看到两种方法都使用了相同的方式:
// Add() Method
this._items[this._size++] = item; 
// Setter method
this._items[index] = value;

_items 是类型为 T 的数组:

private T[] _items;

在构造函数中,_items 被初始化为:

this._items = new T[capacity]

现在,经过这些操作之后,我很好奇为什么我无法按索引将项目添加到列表中,尽管我明确指定了列表容量?

1
索引器不会添加项目。您有一个空列表,并且正在尝试访问(不存在的)第一个项目。 - Panagiotis Kanavos
4个回答

16

原因是你不能使用索引器向列表中添加元素,而是会替换已有的元素。

由于你还没有向列表中添加任何元素,所以它是空的,尝试使用索引器“添加”元素将抛出该异常。

代码示例:

new List<string>(11);

不会创建一个有11个元素的列表,而是创建一个初始容量为11个元素的列表。这是一种优化。如果您添加更多元素,列表将需要在内部调整大小,并且您可以传递期望或已知的容量来避免过多的这些调整。

这里是一个展示的LINQPad程序:

void Main()
{
    var l = new List<string>(10);
    l.Dump(); // empty list

    l.Add("Item");
    l.Dump(); // one item

    l[0] = "Other item";
    l.Dump(); // still one item

    l.Capacity.Dump(); // should be 10
    l.AddRange(Enumerable.Range(1, 20).Select(idx => idx.ToString()));
    l.Capacity.Dump(); // should be 21 or more
}

输出:

linqpad程序的输出


内部机制

在一个 List<T> 内部,实际上使用一个数组来存储元素。此外,为了跟踪已经使用了多少个元素,还保持了一个 Count 属性/值。

当你构造一个空列表时,如果没有传入容量,则会使用默认值作为该数组的初始大小。

随着您不断向列表添加新元素,您将慢慢填充那个数组,直到末尾。一旦填充整个数组,并向其中添加另一个元素,就必须构造一个新的、更大的数组。然后,所有旧数组中的元素都被复制到这个新的、更大的数组中,并从现在开始使用该数组。

这就是为什么内部代码调用了那个 EnsureCapacity 方法。如果需要,这个方法会执行调整大小操作。

每次必须调整数组大小时,都会构造一个新的数组并将所有元素复制过去。随着数组的增长,这个操作的成本也随之增加。虽然它不是非常昂贵,但它确实不是免费的。

因此,如果您知道您将需要在列表中存储1000个元素,那么最好一开始就传入一个容量值。这样,该数组的初始大小可能足够大,从而永远不需要调整大小/替换。同时,也不要只是传入一个非常大的容量值,因为这可能会使用很多不必要的内存。

还要注意,本答案部分的所有内容都是未经记录的(据我所知)行为,因此您从中了解到的任何细节或特定行为都不应该影响您编写的代码,除了关于传递良好容量值的知识。


此外,还有一种使用索引的Insert方法,但如果列表项没有使用虚拟值“初始化”,则会引发相同的异常。 - Nicolas R
你可以在一个空列表上调用Insert,传入索引0。这基本上等同于Add。也就是说,你可以传入Count,它也会起作用。 - Lasse V. Karlsen
1
当然可以。但是如果他尝试执行 Insert(5, "item"),它会抛出异常。 - Nicolas R
谢谢,但我仍然不知道索引器中setter方法的意义在哪里?因为Add方法不使用setter方法,如果我不能使用它,那么它为什么存在? - Selman Genç
2
你可以在向列表添加元素之后使用它。尝试:list.Add("Test"); list[0] = "Other test"; - Lasse V. Karlsen
现在我明白了,谢谢大家 :) 因此,我们不能使用索引器将新元素添加到泛型列表中,但我们可以使用它来更改现有元素。 - Selman Genç

7
不,这并不奇怪。您错误地认为接受容量的构造函数会在列表中初始化那么多元素。实际上它并没有。该列表为空,您需要添加元素。
如果出于某种原因需要使用元素进行初始化,可以这样做:
 new List<string>(Enumerable.Repeat<string>(string.Empty, 11));

听起来 string[] 数组更适合。

那么,我该如何设置大小?如果不能,为什么在该索引器中有一个setter方法? - Selman Genç
容量的关键在于避免内存重分配。设置适当的容量,只需使用添加操作。或者如果必须使用初始化加载空条目。 - James World

6

4

列表的容量和计数是有区别的。

容量表示内部数组的大小。计数表示列表包含的有效项数。您正在使用的构造函数设置容量,但有效项的数量仍为0。索引器根据有效项的数量而不是容量进行检查。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接