简单编程挑战:任意迭代






4.87/5 (10投票s)
提供了一个使用 C# 中的迭代器来任意迭代 .NET 数组元素的代码示例。
引言
您是否编写过代码,该代码专注于逐步遍历数组(无论数组的提供者必须使用多少维度)? 如果您编写过,那么您一定熟悉多次重写同一个方法,只是为了提供相同的计算,但循环遍历它们的循环数量不同,这有多么烦人。 也可以创建一个具有相同功能的递归方法; 但是,此类系统的用户对何时可以跳出循环的控制权较少。
背景
如果您熟悉 C# 2.0 及更高版本,您可能在某种程度上使用过迭代器。 如果没有,请前往 MSDN 阅读它们,它们在当今的环境中越来越有用。
传统上
假设您正在处理的数组来自另一个来源,并且它们非常友善,因此像这样创建了它
int[, , , , , ,] values = (int[, , , , , ,])Array.CreateInstance
(typeof(int), new int[] { 4, 4, 4, 4, 4, 4, 4 }, new int[]
{ -3, 15, 1024, 58, -90, 3, -1 });
如果您熟悉此类数组创建,您就会知道它有七个维度,每个维度有四个项目,总共有 47 (16,384) 个元素,起始索引为: -3, 15, 1024, 58, -90, 3, -1
。
现在,通常要遍历七维数组,您需要设置七个不同的循环,这些循环相互嵌套
for (int i1 = values.GetLowerBound(0), i1M = values.GetUpperBound(0),
i2L = values.GetLowerBound(1), i2M = values.GetUpperBound(1),
i3L = values.GetLowerBound(2), i3M = values.GetUpperBound(2),
i4L = values.GetLowerBound(3), i4M = values.GetUpperBound(3),
i5L = values.GetLowerBound(4), i5M = values.GetUpperBound(4),
i6L = values.GetLowerBound(5), i6M = values.GetUpperBound(5),
i7L = values.GetLowerBound(6), i7M = values.GetUpperBound(6); i1 <= i1M; i1++)
for (int i2 = i2L; i2 <= i2M; i2++)
for (int i3 = i3L; i3 <= i3M; i3++)
for (int i4 = i4L; i4 <= i4M; i4++)
for (int i5 = i5L; i5 <= i5M; i5++)
for (int i6 = i6L; i6 <= i6M; i6++)
for (int i7 = i7L; i7 <= i7M; i7++)
values[i1, i2, i3, i4, i5, i6, i7] *=
values[i1, i2, i3, i4, i5, i6, i7];
我相信您知道,编写此类代码令人恼火。 如果您涵盖了大量不同维度的数组,维护起来也可能会变得困难。
更简单的解决方案
当然,在大多数情况下,通过数组上的 foreach
可以轻松实现数组的迭代。 这样做的问题是您无法更改数组,并且您也不知道在任何给定时刻您在数组中的确切位置。 当然,您可以使用索引来跟踪它,但是该单个索引不能代表该数组中的所有维度。
这就是迭代器的用武之地
/// <summary>
/// Obtains an enumerable object which can iterate through all of
/// the indices of an <see cref="Array"/> regardless of its
/// dimensional complexity.
/// </summary>
/// <param name="array">The <see cref="Array"/> to perform
/// iteration on.</param>
/// <returns>A <see cref="IEnumerable{T}"/> object that
/// yields a single-dimensional array per iteration relative
/// to the <paramref name="array"/> provided.</returns>
/// <remarks><para>The number of values in the resultant array,
/// per iteration, is equivalent to the
/// <see cref="Array.ArrayRank"/> of the
/// <paramref name="array"/> provided.</para>
/// <para>Due to the nature this method was intended to be used,
/// the array retrieved per iteration is the same so it is not
/// guaranteed to be the same on a much later access
/// should its reference be stored, and the iteration
/// continued.</para></remarks>
public static IEnumerable<int[]> Iterate(this Array array)
{
int[] indices;
int rank = array.Rank;
if (rank == 1)
{
/* *
* Simple answer for one dimension
* */
indices = new int[] { array.GetLowerBound(0) };
for (; indices[0] <= array.GetUpperBound(0); indices[0]++)
yield return indices;
}
else
{
/* *
* Multi-dimensional, or non-vector, arrays are a bit different.
* */
indices = new int[array.Rank];
/* *
* Obtain the upper/lower bounds..
* */
int[] upperBounds = new int[array.Rank];
for (int i = 0; i < rank; i++)
{
indices[i] = array.GetLowerBound(i);
upperBounds[i] = array.GetUpperBound(i);
}
int[] lowerBounds = (int[])indices.Clone();
Repeater:
{
/* *
* Nifty thing is... it's always the same array,
* which means there's no performance hit for
* creating and returning new arrays, because we don't.
* */
yield return indices;
/* *
* Move through the dimensions, starting
* with the highest-order.
* */
for (int i = rank - 1; i >= 0; i--)
{
/* *
* Index the current dimension...
* */
indices[i]++;
/* *
* If the current dimension is in range
* we're done.
* */
if (indices[i] <= upperBounds[i])
break;
/* *
* Reset the current index, the loop
* will continue until all 'overflows'
* on the array are hit and reset
* accordingly.
* */
indices[i] = lowerBounds[i];
/* *
* If the first dimension has been incremented
* and exceeded the high point of the dimension,
* then exit stage left.
* */
if (i == 0)
yield break;
}
goto Repeater;
}
}
yield break;
}
数组中的每个点都作为该数组中的一组索引生成。 此外,只返回一个数组实例,从而减少了此类方法的内存占用。
由于它是一个迭代器,而不是递归解决方案,因此从数组中跳出就像跳出任何一个循环一样简单,甚至更容易。
这是一个迭代器的使用示例
foreach (var indices in values.Iterate())
{
int current = (int)values.GetValue(indices);
values.SetValue(current * current, indices);
}
关注点
与维度特定的替代方案相比,使用它会产生一些开销; 但是,在我看来,它给您带来的简单性减轻了这种影响。 开销级别可以忽略不计,例如,以传统方式(使用七个嵌套循环)遍历上述数组需要 0.0002451 秒,而使用迭代器需要 0.0009789 秒。 可能性是您将在该循环中执行的操作将花费比迭代器产生的开销长得多。
我可以看到这在打印数组方面非常有用。 与创建一组循环来打印 n 维数组中的元素相比,您只需使用迭代器,发出每个步骤的索引,并发出与该组索引关联的实际值。
历史
- 2010 年 10 月 12 日 - 1.0 版