Как перебрать два массива одновременно? - PullRequest
14 голосов
/ 30 января 2009

У меня есть два массива при разборе текстового файла. Первый содержит имена столбцов, второй содержит значения из текущей строки. Мне нужно перебрать оба списка одновременно, чтобы построить карту. Прямо сейчас у меня есть следующее:

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

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

Это работает просто отлично, но не совсем удовлетворяет моё чувство элегантности, и становится действительно волосатым, если количество массивов больше двух (как я должен делать иногда). У кого-нибудь есть другая, более краткая идиома?

Ответы [ 6 ]

22 голосов
/ 30 января 2009

В вашем исходном коде есть неочевидная псевдо-ошибка - 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.

EDIT:

При использовании типа 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 ...) отдельно, в связи с тем, как работают дженерики
  • Если вы счастливы использовать один и тот же тип для каждой части, вы можете сделать лучше
17 голосов
/ 30 января 2009

если количество столбцов совпадает с количеством элементов в каждой строке, не могли бы вы использовать цикл 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
}
4 голосов
/ 30 января 2009

На функциональном языке вы обычно находите функцию "zip", которая, будем надеяться, будет частью C # 4.0. Барт де Смет предоставляет забавную реализацию zip на основе существующих функций LINQ:

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 голосов
/ 30 января 2009

Было бы неплохо использовать 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);
}
3 голосов
/ 30 января 2009

Если вы действительно используете массивы, лучше всего просто использовать обычный цикл for с индексами. Не так приятно, конечно, но, насколько я знаю, .NET не предлагает лучшего способа сделать это.

Вы также можете инкапсулировать свой код в метод с именем zip - это обычная функция списка высшего порядка. Тем не менее, в C # отсутствует подходящий тип Tuple, это довольно хитро. В итоге вы вернете IEnumerable<KeyValuePair<T1, T2>>, что не очень приятно.

Кстати, вы действительно используете IEnumerable вместо IEnumerable<T> или почему вы разыгрываете значение Current?

2 голосов
/ 30 января 2009

Вместо создания двух отдельных массивов вы можете создать двумерный массив или словарь (что было бы лучше). Но на самом деле, если это сработает, я бы не стал его менять.

...