为什么 List IndexOf 允许超出范围的起始索引?

Why does List IndexOf allow out-of-range start index?

为什么 List<T>.IndexOf 允许超出范围的起始索引?

var list = new List<int>() { 100 };
Console.WriteLine(list.IndexOf(1/*item*/, 1/*start index*/));

不会有任何例外。但是此集合中没有索引为 1 的项目!只有一项具有 0 索引。 那么,为什么 .Net 允许你这样做呢?

首先,如果有人应该处理无效输入,那是运行时而不是编译器,因为输入是相同的有效类型 (int)。

话虽如此,实际上,看到 IndexOf 的源代码使其看起来像是一个实现错误:

[__DynamicallyInvokable]
public int IndexOf(T item, int index)
{
    if (index > this._size)
    {
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_Index);
    }
    return Array.IndexOf<T>(this._items, item, index, this._size - index);
}

如您所见,它的目的是不允许您插入大于列表大小的无效索引,但比较是使用 > 而不是 >= .

  • 如下代码returns 0:

    var list = new List<int>() { 100 };
    Console.WriteLine(list.IndexOf(100/*item*/, 0/*start index*/));
    
  • 如下代码returns -1:

    var list = new List<int>() { 100 };
    Console.WriteLine(list.IndexOf(100/*item*/, 1/*start index*/));
    
  • 下面的代码抛出一个Exception:

    var list = new List<int>() { 100 };
    Console.WriteLine(list.IndexOf(100/*item*/, 2/*start index*/));
    

第二种和第三种情况没有理由表现得不同这让它看起来像是IndexOf.

的实施

此外,the documentation says

ArgumentOutOfRangeException | index is outside the range of valid indexes for the List<T>.

我们刚刚看到的并不是正在发生的事情

注意:相同的行为发生在数组中:

int[] arr =  { 100 };

//Output: 0
Console.WriteLine(Array.IndexOf(arr, 100/*item*/, 0/*start index*/));

//Output: -1
Console.WriteLine(Array.IndexOf(arr, 100/*item*/, 1/*start index*/));

//Throws ArgumentOutOfRangeException
Console.WriteLine(Array.IndexOf(arr, 100/*item*/, 2/*start index*/));

它允许它是因为有人认为这是可以的,并且有人编写了规范或实现了该方法。

List(T).IndexOf Method中也有一些记录:

0 (zero) is valid in an empty list.

(我也认为 Count 是任何列表的有效起始索引)

请注意,对于 Array.IndexOf Method:

If startIndex equals Array.Length, the method returns -1. If startIndex is greater than Array.Length, the method throws an ArgumentOutOfRangeException.

让我在这里澄清一下我的答案。

你问的是"Why does this method allow this input".

唯一合法的原因是 "Because someone implemented the method so that it did"。

这是一个错误吗?很可能是这样。文档只说 0 是空列表的合法起始索引,并没有直接说 1 对于其中包含单个元素的列表是否合法。该方法的异常文档似乎与此相矛盾(正如在评论中提出的那样),这似乎支持它是一个错误。

但是 "why does it do this" 的唯一原因是有人确实以这种方式实现了该方法。它可能是一个有意识的选择,它可能是一个错误,它可能是代码或文档中的疏忽。

只有实施此方法的人才能知道它是哪一个。

要了解发生了什么,我们可以查看 sources:

public int IndexOf(T item, int index) {
    if (index > _size)
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_Index);
    Contract.Ensures(Contract.Result<int>() >= -1);
    Contract.Ensures(Contract.Result<int>() < Count);
    Contract.EndContractBlock();
    return Array.IndexOf(_items, item, index, _size - index);
}

_size 这里等同于 list.Count 所以当你有一个项目时你可以使用 1index 即使它不存在于列表中.

除非有我没有看到的特殊原因,否则这看起来像是框架中一个很好的旧错误。文档甚至提到如果

应该抛出异常

index is outside the range of valid indexes for the List<T>.

当然,唯一可以肯定地说出这是为什么的人就是做出那个决定的人。

我看到的唯一合乎逻辑的原因(这是我的猜测)是允许这样使用

for (int index = list.IndexOf(value); index >= 0; index = list.IndexOf(value, index + 1))
{
    // do something
}

或者换句话说,能够从上次成功搜索的下一个索引安全地重新开始搜索。

这可能看起来不是很常见的情况,但却是处理字符串时的典型模式(例如,当人们想要避免 Split 时)。这让我想起 String.IndexOf 具有相同的行为并且有更好的记录(尽管没有说明原因):

The startIndex parameter can range from 0 to the length of the string instance. If startIndex equals the length of the string instance, the method returns -1.

继续,因为 ArraystringList<T> 具有相同的行为,显然这是有意的,绝对不是实现错误。

我想,我明白为什么。 实现这样的方法比较容易。 看:

public int IndexOf(T item, int index)
{
    if (index > this._size)
    {
        ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.index, ExceptionResource.ArgumentOutOfRange_Index);
    }
    return Array.IndexOf<T>(this._items, item, index, this._size - index);
}

此方法重载使用另一个更常见的重载:

return Array.IndexOf<T>(this._items, item, index, this._size - index);

所以这个方法也用到了:

public int IndexOf(T item)

所以如果这段代码没有意义:

var list = new List<int>(); /*empty!*/
Console.WriteLine(list.IndexOf(1/*item*/));

将抛出一个 Exception 。但是没有这个许可,就没有办法使用普通重载 IndexOf 的重载。