Ответ 1
Что это?
Это исключение означает, что вы пытаетесь получить доступ к элементу коллекции по индексу с использованием недопустимого индекса. Индекс недействителен, если он ниже, чем нижняя граница коллекции или больше или равно количеству содержащихся в нем элементов.
Когда он брошен
Учитывая массив, объявленный как:
byte[] array = new byte[4];
Вы можете получить доступ к этому массиву от 0 до 3, значения вне этого диапазона вызовут IndexOutOfRangeException
. Помните об этом при создании и доступе к массиву.
Длина массива
В С#, как правило, массивы основаны на 0. Это означает, что первый элемент имеет индекс 0, а последний элемент имеет индекс Length - 1
(где Length
- общее количество элементов в массиве), поэтому этот код не работает:
array[array.Length] = 0;
Кроме того, обратите внимание, что если у вас многомерный массив, вы не можете использовать Array.Length
для обоих измерений, вы должны использовать Array.GetLength()
:
int[,] data = new int[10, 5];
for (int i=0; i < data.GetLength(0); ++i) {
for (int j=0; j < data.GetLength(1); ++j) {
data[i, j] = 1;
}
}
Верхняя граница не включена
В следующем примере мы создаем исходный двумерный массив Color
. Каждый элемент представляет пиксель, индексы от (0, 0)
до (imageWidth - 1, imageHeight - 1)
.
Color[,] pixels = new Color[imageWidth, imageHeight];
for (int x = 0; x <= imageWidth; ++x) {
for (int y = 0; y <= imageHeight; ++y) {
pixels[x, y] = backgroundColor;
}
}
Этот код завершится неудачно, потому что массив будет 0, а последний (внизу справа) пиксель в изображении pixels[imageWidth - 1, imageHeight - 1]
:
pixels[imageWidth, imageHeight] = Color.Black;
В другом сценарии вы можете получить ArgumentOutOfRangeException
для этого кода (например, если вы используете метод GetPixel
в классе Bitmap
).
Массивы не растут
Массив быстро. Очень быстрый в линейном поиске по сравнению с любой другой коллекцией. Это связано с тем, что элементы смежны в памяти, поэтому адрес памяти можно вычислить (а приращение - просто дополнение). Не нужно следовать списку node, простой математике! Вы платите это с ограничением: они не могут расти, если вам нужно больше элементов, вам нужно перераспределить этот массив (это может быть большим, если старые элементы должны быть скопированы в новый блок). Вы изменяете их размер с помощью Array.Resize<T>()
, этот пример добавляет новую запись в существующий массив:
Array.Resize(ref array, array.Length + 1);
Не забывайте, что действующие индексы от 0
до Length - 1
. Если вы просто попробуете назначить элемент в Length
, вы получите IndexOutOfRangeException
(это может смутить вас, если вы думаете, что они могут увеличиться с синтаксисом, аналогичным методу Insert
других коллекций).
Специальные массивы с пользовательской нижней границей
Первый элемент массива всегда имеет индекс 0. Это не всегда верно, потому что вы можете создать массив с пользовательской нижней границей:
var array = Array.CreateInstance(typeof(byte), new int[] { 4 }, new int[] { 1 });
В этом примере показатели массива действительны от 1 до 4. Конечно, верхняя граница не может быть изменена.
Неверные аргументы
Если вы получаете доступ к массиву с использованием неутвержденных аргументов (из пользовательского ввода или из функции пользователя), вы можете получить эту ошибку:
private static string[] RomanNumbers =
new string[] { "I", "II", "III", "IV", "V" };
public static string Romanize(int number)
{
return RomanNumbers[number];
}
Неожиданные результаты
Это исключение может быть вызвано и по другой причине: по соглашению многие функции поиска возвращают -1 (значения nullables были введены с .NET 2.0, и в любом случае это также известное соглашение, используемое с многих лет), если они ничего не нашли, Представьте себе, что у вас есть массив объектов, сравнимый со строкой. Вы можете написать этот код:
// Items comparable with a string
Console.WriteLine("First item equals to 'Debug' is '{0}'.",
myArray[Array.IndexOf(myArray, "Debug")]);
// Arbitrary objects
Console.WriteLine("First item equals to 'Debug' is '{0}'.",
myArray[Array.FindIndex(myArray, x => x.Type == "Debug")]);
Это не удастся, если никакие элементы в myArray
не будут удовлетворять условию поиска, потому что Array.IndexOf()
будет возвращать -1, а затем будет доступ к массиву.
Следующий пример - наивный пример для вычисления вхождений заданного набора чисел (знание максимального числа и возврат массива, где элемент в индексе 0 представляет число 0, элементы в индексе 1 представляют номер 1 и т.д.):
static int[] CountOccurences(int maximum, IEnumerable<int> numbers) {
int[] result = new int[maximum + 1]; // Includes 0
foreach (int number in numbers)
++result[number];
return result;
}
Конечно, это довольно ужасная реализация, но то, что я хочу показать, заключается в том, что она будет терпеть неудачу для отрицательных чисел и чисел выше maximum
.
Как это относится к List<T>
?
Те же случаи, что и массив - диапазон действительных индексов - 0 (List
индексы всегда начинаются с 0) до list.Count
- доступ к элементам вне этого диапазона вызовет исключение.
Обратите внимание, что List<T>
выбрасывает ArgumentOutOfRangeException
для тех же случаев, когда массивы используют IndexOutOfRangeException
.
В отличие от массивов, List<T>
запускается пустым, поэтому попытка доступа к элементам только что созданного списка приводит к этому исключению.
var list = new List<int>();
Общий случай состоит в том, чтобы заполнить список индексацией (аналогично Dictionary<int, T>
), приведет к исключению:
list[0] = 42; // exception
list.Add(42); // correct
IDataReader и столбцы
Представьте, что вы пытаетесь прочитать данные из базы данных с помощью этого кода:
using (var connection = CreateConnection()) {
using (var command = connection.CreateCommand()) {
command.CommandText = "SELECT MyColumn1, MyColumn2 FROM MyTable";
using (var reader = command.ExecuteReader()) {
while (reader.Read()) {
ProcessData(reader.GetString(2)); // Throws!
}
}
}
}
GetString()
будет бросать IndexOutOfRangeException
, потому что у вас набор данных имеет только два столбца, но вы пытаетесь получить значение из третьего (индексы всегда основаны на 0).
Обратите внимание, что это поведение распространяется на большинство реализаций IDataReader
(SqlDataReader
, OleDbDataReader
и т.д.).
Вы можете получить такое же исключение, если вы используете перегрузку IDataReader оператора индексатора, который принимает имя столбца и передает недопустимое имя столбца.
Предположим, например, что вы получили столбец с именем Column1, но затем вы пытаетесь получить значение этого поля с помощью
var data = dr["Colum1"]; // Missing the n in Column1.
Это происходит потому, что оператор индексатора реализован, пытаясь получить индекс поля Colum1, который не существует. Метод GetOrdinal выкинет это исключение, когда его внутренний хелпер-код возвращает -1 как индекс "Colum1".
Другие
Существует другой (документированный) случай, когда это исключение выбрано: if, in DataView
, имя столбца данных, переданное в свойство DataViewSort
, недопустимо.
Как избежать
В этих примерах позвольте мне для простоты предположить, что массивы всегда монодиальны и основаны на 0. Если вы хотите быть строгим (или разрабатываете библиотеку), вам может потребоваться заменить 0
на GetLowerBound(0)
и .Length
на GetUpperBound(0)
(конечно, если у вас есть параметры типа System.Arra
y, это не применяется для T[]
). Обратите внимание, что в этом случае верхняя граница включена, тогда этот код:
for (int i=0; i < array.Length; ++i) { }
Следует переписать следующим образом:
for (int i=array.GetLowerBound(0); i <= array.GetUpperBound(0); ++i) { }
Обратите внимание, что это недопустимо (оно будет бросать InvalidCastException
), поэтому, если ваши параметры T[]
, вы можете быть уверены в пользовательских матрицах с нижними границами:
void foo<T>(T[] array) { }
void test() {
// This will throw InvalidCastException, cannot convert Int32[] to Int32[*]
foo((int)Array.CreateInstance(typeof(int), new int[] { 1 }, new int[] { 1 }));
}
Подтвердить параметры
Если индекс поступает из параметра, вы всегда должны его проверять (бросая соответствующие ArgumentException
или ArgumentOutOfRangeException
). В следующем примере неправильные параметры могут вызвать IndexOutOfRangeException
, пользователи этой функции могут ожидать этого, потому что они передают массив, но это не всегда так очевидно. Я бы предложил всегда проверять параметры для публичных функций:
static void SetRange<T>(T[] array, int from, int length, Func<i, T> function)
{
if (from < 0 || from>= array.Length)
throw new ArgumentOutOfRangeException("from");
if (length < 0)
throw new ArgumentOutOfRangeException("length");
if (from + length > array.Length)
throw new ArgumentException("...");
for (int i=from; i < from + length; ++i)
array[i] = function(i);
}
Если функция является частной, вы можете просто заменить логику if
на Debug.Assert()
:
Debug.Assert(from >= 0 && from < array.Length);
Проверить состояние объекта
Индекс массива может не поступать непосредственно из параметра. Он может быть частью состояния объекта. В общем, всегда хорошая практика для проверки состояния объекта (сама по себе и с параметрами функции, если это необходимо). Вы можете использовать Debug.Assert()
, создать правильное исключение (более описательное из-за проблемы) или обработать как в этом примере:
class Table {
public int SelectedIndex { get; set; }
public Row[] Rows { get; set; }
public Row SelectedRow {
get {
if (Rows == null)
throw new InvalidOperationException("...");
// No or wrong selection, here we just return null for
// this case (it may be the reason we use this property
// instead of direct access)
if (SelectedIndex < 0 || SelectedIndex >= Rows.Length)
return null;
return Rows[SelectedIndex];
}
}
Подтвердить возвращаемые значения
В одном из предыдущих примеров мы непосредственно использовали возвращаемое значение Array.IndexOf()
. Если мы знаем, что это может потерпеть неудачу, лучше справиться с этим:
int index = myArray[Array.IndexOf(myArray, "Debug");
if (index != -1) { } else { }
Отладка
На мой взгляд, большинство вопросов, здесь, на SO, об этой ошибке можно просто избежать. Время, которое вы потратили, чтобы написать правильный вопрос (с небольшим рабочим примером и небольшим объяснением), может быть намного больше, чем время, которое вам нужно будет отладить ваш код. Прежде всего прочитайте это сообщение в блоге Eric Lippert о отладке небольших программ, я не буду повторять его слова здесь, но это абсолютно необходимо прочитать.
У вас есть исходный код, у вас есть сообщение об исключении со стеком. Идите туда, выберите номер правой линии, и вы увидите:
array[index] = newValue;
Вы нашли свою ошибку, проверьте, как index
увеличивается. Это правильно? Проверьте, как распределяется массив, соответствует ли значение index
? Правильно ли это в соответствии с вашей спецификацией? Если вы ответите "да" на все эти вопросы, вы найдете здесь полезную помощь в StackOverflow, но сначала проверьте это самостоятельно. Вы сэкономите свое время!
Хорошая стартовая точка - всегда использовать утверждения и проверять входные данные. Вы даже можете использовать кодовые контракты. Когда что-то пошло не так, и вы не можете понять, что происходит с быстрым взглядом на ваш код, тогда вам нужно прибегнуть к старому другу: отладчик. Просто запустите приложение в отладке внутри Visual Studio (или вашей любимой IDE), вы увидите, какая именно строка генерирует это исключение, какой массив задействован и какой индекс вы пытаетесь использовать. В самом деле, в 99% случаев вы сами решите это через несколько минут.
Если это происходит в процессе производства, вам лучше добавить утверждения в инкриминируемый код, возможно, мы не увидим в вашем коде то, что вы не видите сами (но вы всегда можете сделать ставку).