19

Why does List<T>.IndexOf allow out-of-range start index?

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

There will not be any exceptions. But there is no item with 1 index in this collection! There is just one item with 0 index. So, why does .Net allow you to do it?

Tamir Vered
  • 10,187
  • 5
  • 45
  • 57

5 Answers5

13

First of all, if someone should take care of an invalid input it's the runtime and not the compiler since the input is of the same valid type (int).

With that said, actually, seeing the source code of IndexOf making it seem like an implementation bug:

[__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);
}

As you can see, it was intended to not allow you to insert an invalid index which is bigger than the size of the list, but the comparison is done with > instead of >=.

  • The following code returns 0:

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

    var list = new List<int>() { 100 };
    Console.WriteLine(list.IndexOf(100/*item*/, 1/*start index*/));
    
  • While The following code throws an Exception:

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

There is no reason what so ever for the second and third cases to behave differently which makes it seem as a bug in the implementation of IndexOf.

Also, the documentation says:

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

Which as we have just seen is not what happening.

Note: the same behaviour happens with arrays:

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*/));
Tamir Vered
  • 10,187
  • 5
  • 45
  • 57
  • ...or it's just a design decision and not a bug as you are very quick to conclude. If the docs specifically state that 0 (== `.Count`) is valid for an empty list, it only makes sense to allow the same value as `.Count` for all lists. – Matti Virkkunen Feb 21 '16 at 11:34
  • 1
    Give me a reasonable reason to allow `size` and throw on `size + 1` when indexes are zero based. – Tamir Vered Feb 21 '16 at 11:54
  • It's an option, still the same result could be achieved by adding a validation to the `for` loop, still it is very minor reason to allow such an odd behavior... – Tamir Vered Feb 21 '16 at 12:36
  • This is not odd behavior. You have array with 3 elements. If you start on index 0 you have skip 0 elements and there are 3 to go. If you start at 3 you have skip 3 elements and there are 0 to go. Why would you prohibit starting at the end of collection? – abc667 Feb 24 '16 at 17:43
  • Intuitively if you approach an index which is out of the `Array`/`List` It should throw an `Exception`. If It doesn't - There should not be a difference between index which is too large by 1 or by 30. – Tamir Vered Feb 24 '16 at 17:51
  • I think you are thinking about this method like IndexOf(item, startAtIndex) but its really IndexOf(item, startBeforeIndex). – abc667 Feb 24 '16 at 18:04
  • It's called `startIndex`. That's just my interpretation you don't have to agree... – Tamir Vered Feb 24 '16 at 18:11
9

It allows it because someone decided this was OK, and that someone either wrote the spec or implemented the method.

It is also somewhat documented in List(T).IndexOf Method:

0 (zero) is valid in an empty list.

(which I also take that Count is a valid start index for any list)

Note that the same thing is documented, but slightly better documented, for Array.IndexOf Method:

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

Let me clarify my answer here.

You're asking "Why does this method allow this input".

The only legal reason is "Because someone implemented the method so that it did".

Is it a bug? It may very well be. The documentation only says that 0 is legal start index for an empty list, it does not directly say that 1 is legal or not for a list with a single element in it. The exception documentation for the method seems to contradict this (as has been brought up in comments) which seems to be in favor of it being a bug.

But the only reason for "why does it do this" is that someone actually implemented the method that way. It may be a conscious choice, it may be a bug, it may be an oversight either in the code or in the documentation.

The only one that can tell which one it is would be the person or people that implemented this method.

Tamir Vered
  • 10,187
  • 5
  • 45
  • 57
Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
6

Of course the only one who can say for sure why is this are the people that made that decision.

The only logical reason I see (and that's my guess) is to allow usage like this

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

or in other words, to be able to restart safely the search from the next index of the last successful search.

This might not look a very common scenario, but is typical pattern when processing strings (for instance when one want to avoid Split). Which reminds me that String.IndexOf has the same behavior and is a bit better documented (although without specifying the reason):

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.

To resume, since Array, string and List<T> share the same behavior, apparently it's intended and definitely not an implementation bug.

Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
3

To understand what's happening we can take a look at the 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 here is equivalent to list.Count so when you have one item you can use an index of 1 even if it doesn't exist in the list.

Unless there is a special reason that I'm not seeing, this looks like a good old off-by-one error in the framework. The documentation even mentions that an exceptions should be thrown if

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

dee-see
  • 23,668
  • 5
  • 58
  • 91
1

I think, I understand why. It's kind of easier whay of realisation of methods like this. Look:

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);
}

This method overload use another, more common, overload:

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

So this method also use it:

public int IndexOf(T item)

So it doens't make sence if this code:

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

will throw an Exception . But there is no way to use this overload of IndexOf using common overload without this admission.

Tamir Vered
  • 10,187
  • 5
  • 45
  • 57