【问题标题】:How to iterate over two arrays at once?如何一次遍历两个数组?
【发布时间】:2010-10-04 13:06:38
【问题描述】:

我在解析文本文件时构建了两个数组。第一个包含列名,第二个包含当前行的值。我需要一次遍历两个列表来构建地图。现在我有以下内容:

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

foreach (String column in columnList)
{
    valueEnumerator.MoveNext();
    valueMap.Add(column, (String)valueEnumerator.Current);
}

这工作得很好,但它并不能完全满足我的优雅感,如果数组的数量大于两个(我有时必须这样做),它会变得非常毛茸茸。有没有人有另一个更简洁的成语?

【问题讨论】:

标签: c# iterator enumerator


【解决方案1】:

如果您真的在使用数组,最好的方法可能就是使用带有索引的传统for 循环。没有那么好,当然,但据我所知,.NET 并没有提供更好的方法。

您还可以将您的代码封装到一个名为zip 的方法中——这是一个常见的高阶列表函数。但是,C# 缺少合适的 Tuple 类型,这很麻烦。你最终会返回一个不太好的IEnumerable<KeyValuePair<T1, T2>>

顺便问一下,你真的使用IEnumerable 而不是IEnumerable<T> 还是为什么要转换Current 值?

【讨论】:

    【解决方案2】:

    如果列名的数量与每行中的元素数量相同,你能不使用 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
    }
    

    【讨论】:

    • 我建议在进入这个循环之前检查两个数组的长度是否相同。
    • 虽然我没有在实际的代码示例中包含该检查,但我确实在我的回复开头说过;)不过,点了!
    【解决方案3】:

    您可以创建一个二维数组或字典(这样会更好),而不是创建两个单独的数组。但实际上,如果它有效,我不会尝试改变它。

    【讨论】:

      【解决方案4】:

      您的初始代码中有一个不明显的伪错误 - IEnumerator&lt;T&gt; 扩展了 IDisposable,因此您应该处理它。这对于迭代器块非常重要!数组不是问题,但其他 IEnumerable&lt;T&gt; 实现会出现问题。

      我会这样做:

      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 版本,就像 ActionFunc 代表最多 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...)
      • 如果您乐于为每个部件使用相同的类型,您可以做得更好

      【讨论】:

      • 哇,大量的矫枉过正,它仍然无法与两个以上的阵列一起使用。
      • 你是对的,它不能用于两个以上的数组 - 但它很优雅,并且可高度重复用于两个数组。我认为它实际上可能在 .NET 4.0 中 - 这是 MS 打算包含在 LINQ 中的运算符...
      • (或者对于任何两个序列,我应该说 - 不仅仅是数组。它流式传输数据,因此它可以用于潜在的无限数据源。)
      • 当单个 for 循环可能工作时,它看起来过于复杂。
      • @epochwolf:如果你只打算使用一次,那么 for 循环肯定会更好。不过,这可以说是 LINQ 中的所有内容。重点是可重用性。使用 Pair 类型,调用代码会更简单。稍后会添加到我的答案中。
      【解决方案5】:

      对两者都使用 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);
      }
      

      【讨论】:

        【解决方案6】:

        在函数式语言中,您通常会找到一个“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});
        

        【讨论】:

          猜你喜欢
          • 2015-11-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-04-23
          • 2011-04-04
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多