什么是IndexOutOfRangeException / ArgumentOutOfRangeException以及如何解决?

时间:2014-01-06 00:19:21

标签: c# .net indexoutofrangeexception

我有一些代码,当它执行时,会抛出一个IndexOutOfRangeException,说,

  

索引超出了数组的范围。

这是什么意思,我能做些什么呢?

根据使用的类别,它也可以是ArgumentOutOfRangeException

  

mscorlib.dll中发生了'System.ArgumentOutOfRangeException'类型的异常但未在用户代码中处理附加信息:索引超出范围。必须是非负数且小于集合的大小。

4 个答案:

答案 0 :(得分:201)

它是什么?

此异常表示您尝试使用无效索引按索引访问集合项。当索引低于集合的下限或大于或等于其包含的元素数时,索引无效。

何时被抛出

给定一个声明为:

的数组
byte[] array = new byte[4];

您可以从0到3访问此数组,超出此范围的值将导致IndexOutOfRangeException被抛出。在创建和访问数组时请记住这一点。

数组长度
在C#中,通常,数组是基于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方法)。

数组不会增长
阵列很快。与其他所有系列相比,线性搜索速度非常快。这是因为项目在内存中是连续的,因此可以计算内存地址(并且增量只是一个补充)。无需遵循节点列表,简单的数学!你支付这个限制:它们不能增长,如果你需要更多的元素来重新分配那个数组(如果必须将旧项目复制到新的块中,这可能会很大)。您使用Array.Resize<T>()调整它们的大小,此示例向现有数组添加新条目:

Array.Resize(ref array, array.Length + 1);

请勿忘记有效索引是从0Length - 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(在.NET 2.0中引入了nullables,无论如何它也是一个众所周知的使用惯例多年来如果他们没有找到任何东西。让我们假设你有一个与字符串相当的对象数组。您可以考虑编写此代码:

// 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&#39; s索引总是以0开头)到list.Count - 访问此范围之外的元素将导致异常。< / p>

请注意,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-based)。

请注意,此行为与大多数IDataReader实施共享(SqlDataReaderOleDbDataReader等等)。

如果使用带有列名并传递无效列名的索引器运算符的IDataReader重载,也可以获得相同的异常。
例如,假设您已检索到名为 Column1 的列,但随后尝试使用

检索该字段的值
 var data = dr["Colum1"];  // Missing the n in Column1.

这是因为实现了索引器运算符,试图检索不存在的 Colum1 字段的索引。当其内部帮助程序代码返回-1作为&#34; Colum1&#34;的索引时,GetOrdinal方法将抛出此异常。

<强>其他
抛出此异常时还有另一个(已记录的)情况:如果在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 }));
}

验证参数
如果索引来自参数,则应始终对其进行验证(抛出适当的ArgumentExceptionArgumentOutOfRangeException)。在下一个示例中,错误的参数可能会导致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 { }

如何调试

在我看来,大多数关于此错误的问题可以简单地避免。花在编写正确问题上的时间(使用一个小的工作示例和一个小的解释)可能比您需要调试代码的时间更多。首先阅读Eric Lippert关于debugging of small programs的博客文章,我不会在这里重复他的话,但它绝对是必须阅读

您有源代码,您有包含堆栈跟踪的异常消息。去那里,选择正确的行号,你会看到:

array[index] = newValue;

您找到了错误,请检查index的增加情况。这样对吗?检查数组的分配方式,与index的增加方式是否一致?根据你的指定是否正确?如果您对所有这些问题回答,那么您可以在StackOverflow上找到很好的帮助,但请首先自行检查。你会节省自己的时间!

一个好的起点是始终使用断言并验证输入。您甚至可能希望使用代码合同。当出现问题并且您无法通过快速查看代码来了解会发生什么事情时,您必须求助于一位老朋友:调试器。只需在Visual Studio(或您喜欢的IDE)中的调试中运行您的应用程序,您就会确切地看到哪一行抛出此异常,涉及哪个数组以及您尝试使用哪个索引。真的,有99%的时间你会在几分钟内自己解决它。

如果在生产中发生这种情况,那么您最好在有罪的代码中添加断言,可能我们不会在您的代码中看到您自己无法看到的内容(但您可以随时下注)

答案 1 :(得分:16)

关于Index out of bound of exception的简单解释:

试想一下火车的隔间是D1,D2,D3。 一名乘客来到火车,他有D4的机票。 现在会发生什么。乘客想要进入一个不存在的隔间,所以很明显会出现问题。

相同的场景:每当我们尝试访问数组列表等时,我们只能访问数组中的现有索引。 array[0]array[1]已存在。如果我们尝试访问array[3],它实际上并不存在,因此会出现索引超出范围的异常。

答案 2 :(得分:7)

为了轻松理解这个问题,想象一下我们编写了这段代码:

static void Main(string[] args)
{
    string[] test = new string[3];
    test[0]= "hello1";
    test[1]= "hello2";
    test[2]= "hello3";

    for (int i = 0; i <= 3; i++)
    {
        Console.WriteLine(test[i].ToString());
    }
}

结果将是:

hello1
hello2
hello3

Unhandled Exception: System.IndexOutOfRangeException: Index was outside the bounds of the array.

数组的大小为3(索引0,1和2),但for循环循环4次(0,1,2和3)。
因此,当它尝试在(3)之外访问边界时,它会抛出异常。

答案 3 :(得分:0)

与许多其他异常类型相比,在非常长的完整接受答案中,有一个关于IndexOutOfRangeException的重要观点,即:

通常存在复杂的程序状态,可能难以控制代码中的特定点,例如DB连接断开,因此无法检索输入数据等。此类问题通常导致Exception某种必须冒出一个更高层次的泡沫,因为在这种情况下无法进行处理。

IndexOutOfRangeException通常是不同的,因为在大多数情况下,在引发异常的地方进行检查非常简单。通常,这种异常会被某些代码抛出,这些代码可以很容易地在发生问题的地方进行处理-只需检查数组的实际长度即可。您不想通过更高级别地处理此异常来“解决”此问题,而是通过确保不在第一实例中抛出该异常来解决此问题,在大多数情况下,通过检查数组的长度很容易做到这一点。

另一种说法是,由于对输入或程序状态BUT IndexOutOfRangeException缺乏真正的控制可能会导致其他异常,而不仅仅是飞行员(编程)错误。