The valid range
A List<T> with Count items has the valid indexes 0 through Count - 1. Any other index is out of bounds. The program checks the index each time it runs, and an invalid one throws an ArgumentOutOfRangeException.
List<int> ourArray = new List<int> {50, 40, 30};
// Valid indexes: 0, 1, 2
Console.WriteLine(ourArray[0]); // 50
Console.WriteLine(ourArray[2]); // 30
// Invalid indexes: 3 and above, and anything negative
Console.WriteLine(ourArray[3]); // throws ArgumentOutOfRangeException
The message says Index was out of range. Must be non-negative and less than the size of the collection. A negative index fails the same way, because C# lists don't count backwards from the end.
Don't confuse this with arrays. An invalid index on an array throws IndexOutOfRangeException, while a list throws ArgumentOutOfRangeException. Both mean the same mistake, and both are thrown at run time, not caught by the compiler. If the exception isn't caught, the program stops at that line.
The exercise, step by step
The starter code should print the last element of ages but crashes:
List<int> ages = new List<int>(new[] {14, 32, 50, 21});
Console.WriteLine(ages[ages.Count]);
- Find the index.
ages.Countis 4, but the indexes are 0, 1, 2 and 3. Index 4 is one past the end. - Step back by one. The last item is at
Count - 1, which is 3. Usingages.Count - 1reads 21.
The fixed program:
List<int> ages = new List<int>(new[] {14, 32, 50, 21});
Console.WriteLine(ages[ages.Count - 1]);
It prints:
21
Guarding against the error
You can check the index before using it. The check has two parts, because both ends of the range matter:
List<int> ages = new List<int> {14, 32, 50, 21};
int index = 4;
if (index >= 0 && index < ages.Count) {
Console.WriteLine(ages[index]);
} else {
Console.WriteLine("No such position");
}
This prints No such position. You could also catch the exception with try and catch (ArgumentOutOfRangeException), but a plain check is simpler and shows your intent.
When the exception does happen, read its text first. It names the exception type, and the stack trace points to the line that used the bad index. Then work out which index you passed and compare it with the list's count. Remember that the count can change while the program runs, so the same line may work at one moment and fail at another.
Because the check happens at run time, a program can work for months and fail the first time it receives an unusual input, such as an empty list. Testing small and empty inputs is therefore the best way to find these bugs early.
Common causes
- Using the count as an index. The count is a total, and the last index is one less.
- Loops that go one step too far. A condition of
i <= list.Countreaches the invalid index. Usei < list.Count. - Empty lists. With a count of 0 there is no valid index, and
list[list.Count - 1]asks for index -1. - Lists that shrink. The valid range changes as items are removed. An index that was fine before a
RemoveAtcan be out of range afterwards. - Assigning to a new position.
list[list.Count] = 5;also throws. A list grows withAdd, not by assigning past the end.
Why C# checks bounds
Some languages let a program read past the end of a collection and silently return whatever happens to be in memory. C# never does that. Every list access is checked, so a mistake produces a clear exception with a line number, not wrong results that appear much later. The check is the reason the error message exists in the first place.
Things to try
Try these on a four-item list to see which ones throw. The index 0 works, the index 3 works, and the indexes 4 and -1 throw. Then try list[list.Count - 1] on a list created with new List<int>() and watch it fail at index -1.
Key takeaways
- Valid list indexes run from 0 to
Count - 1. - An invalid index throws
ArgumentOutOfRangeExceptionat run time. - To get the last item, use
list[list.Count - 1], after checking that the list isn't empty.