为什么列表索引允许超出范围的起始索引

本文关键字:索引 范围 许超出 列表 为什么 | 更新日期: 2023-09-27 18:35:28

为什么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);
}

如您所见,它旨在不允许您插入大于列表大小的无效索引,但比较是用>而不是>=完成的。

  • 以下代码返回0

    var list = new List<int>() { 100 };
    Console.WriteLine(list.IndexOf(100/*item*/, 0/*start index*/));
    
  • 以下代码返回-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实现中的一个错误。

此外,文档还说:

ArgumentOutOfRangeException | index 超出了 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方法中有所记录:

0(零)在空列表中有效。

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

请注意,对于

Array.IndexOf方法,记录了同样的事情,但记录得稍微好一些:

如果startIndex等于 Array.Length ,则该方法返回 -1。如果startIndex大于 Array.Length,该方法会抛出一个ArgumentOutOfRangeException

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

您正在询问"为什么此方法允许此输入"。

唯一的法律理由是"因为有人实现了该方法,所以它做到了"。

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

但是"为什么这样做"的唯一原因是有人实际上以这种方式实现了该方法。这可能是一个有意识的选择,它可能是一个错误,它可能是代码或文档中的疏忽。

唯一可以分辨出它是哪一个的人是实现此方法的人。

当然,唯一能确定为什么会说出这个决定的人。

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

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

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

这可能看起来不是很常见的场景,但这是处理字符串时的典型模式(例如,当想要避免Split时)。这提醒我 String.IndexOf 具有相同的行为,并且记录得更好一些(尽管没有指定原因):

startIndex 参数的范围可以从 0 到字符串实例的长度。如果 startIndex 等于字符串实例的长度,则该方法返回 -1。

恢复,因为 ArraystringList<T>共享相同的行为,显然这是有意的,绝对不是实现错误。

要了解正在发生的事情,我们可以查看来源:

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因此,当您有一个项目时,即使列表中不存在1,也可以使用 index

除非有我没有看到的特殊原因,否则这看起来像是框架中的一个老错误。文档甚至提到,如果出现以下情况,则应引发异常

index超出了List<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的重载来使用普通的重载。