15

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

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

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

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

4

6 回答 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 版本,就像最多 4 个委托参数的委托一样 - 但您Action可以Func不要随意。

但是,如果这些值都是相同的类型——并且你很乐意坚持使用数组——那就很容易了。(非数组也可以,但是您不能提前进行长度检查。)您可以这样做:

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...)
  • 如果你乐于为每个部分使用相同的类型,你可以做得更好
于 2009-01-30T19:01:29.187 回答
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
}
于 2009-01-30T18:56:00.197 回答
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});
于 2009-01-30T19:48:39.153 回答
3

如果你真的在使用数组,最好的方法可能就是使用for带有索引的常规循环。没有那么好,当然,但据我所知.NET 并没有提供更好的方法来做到这一点。

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

顺便说一句,您是否真的使用IEnumerable而不是IEnumerable<T>或为什么要转换该Current值?

于 2009-01-30T18:55:39.167 回答
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);
}
于 2009-01-30T19:32:24.240 回答
2

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

于 2009-01-30T18:56:24.080 回答