如何同时迭代两个数组?

15

我有两个数组,在解析文本文件时构建。第一个包含列名,第二个包含当前行的值。我需要同时遍历这两个列表以构建映射。目前我有以下代码:

var currentValues = currentRow.Split(separatorChar);
var valueEnumerator = currentValues.GetEnumerator();

foreach (String column in columnList)
{
    valueEnumerator.MoveNext();
    valueMap.Add(column, (String)valueEnumerator.Current);
}
这个代码能正常工作,但它并不完全满足我的优雅感,并且如果数组数量大于两个(有时需要这样做),则会变得非常混乱。有人有其他更简洁的说法吗?

你可以创建一个类似于<a href="http://www.codeproject.com/KB/collections/Pairenumerable.aspx">Pairenumerable</a>的模板枚举器。个人认为这有些过度设计了。 - Brian
6个回答

23
你的初始代码中存在一个不明显的伪错误-IEnumerator<T>扩展了IDisposable,因此您应该处理它。这在迭代块中非常重要!对于数组来说没有问题,但对于其他IEnumerable<T>实现有问题。
我会这样做:
public static IEnumerable<TResult> PairUp<TFirst,TSecond,TResult>
    (this IEnumerable<TFirst> source, IEnumerable<TSecond> secondSequence,
     Func<TFirst,TSecond,TResult> projection)
{
    using (IEnumerator<TSecond> secondIter = secondSequence.GetEnumerator())
    {
        foreach (TFirst first in source)
        {
            if (!secondIter.MoveNext())
            {
                throw new ArgumentException
                    ("First sequence longer than second");
            }
            yield return projection(first, secondIter.Current);
        }
        if (secondIter.MoveNext())
        {
            throw new ArgumentException
                ("Second sequence longer than first");
        }
    }        
}

然后,每当您需要时,都可以重复使用这个功能:
foreach (var pair in columnList.PairUp(currentRow.Split(separatorChar),
             (column, value) => new { column, value })
{
    // Do something
}

或者您可以创建一个通用的Pair类型,并在PairUp方法中去掉投影参数。

编辑:

使用Pair类型,调用代码将如下所示:

foreach (var pair in columnList.PairUp(currentRow.Split(separatorChar))
{
    // column = pair.First, value = pair.Second
}

看起来这就是最简单的方式。是的,你需要把这个实用方法放在某个地方以便重复使用。在我看来这不是问题。现在考虑多个数组的情况...

如果这些数组是不同类型的,我们会遇到问题。你不能在泛型方法/类型声明中表达任意数量的类型参数 —— 你可以为想要的任意数量的类型参数编写 PairUp 的版本,就像有 Action 和 Func 委托可以处理多达4个委托参数一样 —— 但你无法使其成为任意的。

然而,如果所有的值都是相同类型的,并且你愿意坚持使用数组,那么很容易。(非数组也可以,但你不能提前检查长度。)你可以这样做:

public static IEnumerable<T[]> Zip<T>(params T[][] sources)
{
    // (Insert error checking code here for null or empty sources parameter)

    int length = sources[0].Length;
    if (!sources.All(array => array.Length == length))
    {
        throw new ArgumentException("Arrays must all be of the same length");
    }

    for (int i=0; i < length; i++)
    {
        // Could do this bit with LINQ if you wanted
        T[] result = new T[sources.Length];
        for (int j=0; j < result.Length; j++)
        {
             result[j] = sources[j][i];
        }
        yield return result;
    }
}

那么调用代码将会是:

foreach (var array in Zip(columns, row, whatevers))
{
    // column = array[0]
    // value = array[1]
    // whatever = array[2]
}

当然,这涉及到一定的复制 - 每次创建一个数组。您可以通过引入另一种类型来改变它,如下所示:

public struct Snapshot<T>
{
    readonly T[][] sources;
    readonly int index;

    public Snapshot(T[][] sources, int index)
    {
        this.sources = sources;
        this.index = index;
    }

    public T this[int element]
    {
        return sources[element][index];
    }
}

这可能会被大多数人视为过度设计 ;)
说实话,我可以不断想出各种想法...但基本原则是:
- 通过一些可重用的工作,你可以让调用代码更加优美。 - 对于任意类型的组合,由于泛型的工作方式,你必须分别处理每个参数数量(2、3、4...)。 - 如果你愿意为每个部分使用相同的类型,你可以做得更好。

(或者对于任何两个序列,我应该说-不仅仅是数组。它可以流式传输数据,因此可以处理潜在的无限数据源。) - Jon Skeet
它非常优雅,以至于它错过了它相当特定的用途。 - Hippiehunter
+1 这完美地回答了提问者的问题。而且,我在一百万年里都想不出这个答案。 - Jared
2
一个单独的for循环可能就能解决问题,但看起来过于复杂了。 - epochwolf
1
@epochwolf:如果你只打算使用它一次,那么使用for循环可能更好。不过这也适用于LINQ中的所有内容。重点在于可重用性。使用Pair类型后,调用代码甚至会更简单。稍后会在我的答案中添加。 - Jon Skeet
显示剩余7条评论

17

如果每行元素的数量与列名相同,那么是否可以不使用for循环?

var currentValues = currentRow.Split(separatorChar);

for(var i=0;i<columnList.Length;i++){
   // use i to index both (or all) arrays and build your map
}

5
在进入此循环之前,我建议检查两个数组的长度是否相同。 - James McMahon
虽然我在实际代码示例中没有包含那个检查,但我在回答开头就已经说过了 ;) 不过,你的意见我已经听取了! - brettkelly

4
在函数式编程语言中,通常会找到一个“zip”函数,希望它能成为C#4.0的一部分。Bart de Smet基于现有的LINQ函数提供了一个有趣的zip实现。
public static IEnumerable<TResult> Zip<TFirst, TSecond, TResult>(
  this IEnumerable<TFirst> first, 
  IEnumerable<TSecond> second, 
  Func<TFirst, TSecond, TResult> func)
{
  return first.Select((x, i) => new { X = x, I = i })
    .Join(second.Select((x, i) => new { X = x, I = i }), 
    o => o.I, 
    i => i.I, 
    (o, i) => func(o.X, i.X));
}

然后你可以这样做:

  int[] s1 = new [] { 1, 2, 3 };
  int[] s2 = new[] { 4, 5, 6 };
  var result = s1.Zip(s2, (i1, i2) => new {Value1 = i1, Value2 = i2});

3
如果您确实在使用数组,最好的方法可能就是使用常规的 for 循环和索引。虽然不太好看,但据我所知,.NET 没有更好的方法来完成这个任务。
您还可以将代码封装到名为 zip 的方法中 - 这是一种常见的高阶列表函数。然而,由于 C# 缺乏合适的 Tuple 类型,因此这很麻烦。您最终会返回一个 IEnumerable<KeyValuePair<T1, T2>>,这并不是很好。
顺便问一下,您真的在使用 IEnumerable 而不是 IEnumerable<T> 吗?或者您为什么要对 Current 值进行转换?

3

最好同时使用IEnumerator

var currentValues = currentRow.Split(separatorChar);
using (IEnumerator<string> valueEnum = currentValues.GetEnumerator(), columnEnum = columnList.GetEnumerator()) {
    while (valueEnum.MoveNext() && columnEnum.MoveNext())
        valueMap.Add(columnEnum.Current, valueEnum.Current);
}

或者创建扩展方法

public static IEnumerable<TResult> Zip<T1, T2, TResult>(this IEnumerable<T1> source, IEnumerable<T2> other, Func<T1, T2, TResult> selector) {
    using (IEnumerator<T1> sourceEnum = source.GetEnumerator()) {
        using (IEnumerator<T2> otherEnum = other.GetEnumerator()) {
            while (sourceEnum.MoveNext() && columnEnum.MoveNext())
                yield return selector(sourceEnum.Current, otherEnum.Current);
        }
    }
}

使用方法

var currentValues = currentRow.Split(separatorChar);
foreach (var valueColumnPair in currentValues.Zip(columnList, (a, b) => new { Value = a, Column = b }) {
    valueMap.Add(valueColumnPair.Column, valueColumnPair.Value);
}

2

不需要创建两个单独的数组,可以使用二维数组或字典(后者更好)。但是,如果它能够正常工作,我不会尝试进行更改。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接