Как выполнить итерацию сразу по двум массивам?
-
20-08-2019 - |
Вопрос
У меня есть два массива, созданных при разборе текстового файла.Первый содержит имена столбцов, второй содержит значения из текущей строки.Мне нужно выполнить итерацию по обоим спискам одновременно, чтобы построить карту.Прямо сейчас у меня есть следующее:
var currentValues = currentRow.Split(separatorChar);
var valueEnumerator = currentValues.GetEnumerator();
foreach (String column in columnList)
{
valueEnumerator.MoveNext();
valueMap.Add(column, (String)valueEnumerator.Current);
}
Это работает просто отлично, но это не совсем удовлетворяет моему чувству элегантности, и это становится действительно сложным, если количество массивов больше двух (как мне иногда приходится делать).У кого-нибудь есть другая, более лаконичная идиома?
Решение
если имен столбцов столько же, сколько элементов в каждой строке, не могли бы вы не использовать цикл 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
}
Другие советы
У вас неочевидная псевдо-ошибка в вашем исходном коде - 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 и избавиться от параметра projection в методе 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 ...) отдельно из-за того, как работают дженерики
- Если вы с удовольствием используете один и тот же тип для каждой детали, вы сможете добиться большего
В функциональном языке вы обычно найдете функцию "zip", которая, как мы надеемся, станет частью C # 4.0 . Bart de Smet предоставляет забавную реализацию 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});
Если вы действительно используете массивы, лучший способ, вероятно, просто использовать обычный for
цикл с индексами.Не так приятно, конечно, но, насколько я знаю, .NET не предлагает лучшего способа сделать это.
Вы также могли бы инкапсулировать свой код в метод, вызываемый zip
– это обычная функция списка более высокого порядка.Однако в C # отсутствует подходящий тип кортежа, это довольно грубо.Вы бы в конечном итоге вернули IEnumerable<KeyValuePair<T1, T2>>
что не очень приятно.
Кстати, вы действительно используете IEnumerable
вместо того , чтобы IEnumerable<T>
или почему вы разыгрываете Current
ценность?
Было бы неплохо использовать 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);
}
Вместо создания двух отдельных массивов вы могли бы создать двумерный массив или словарь (что было бы лучше).Но на самом деле, если это сработает, я бы не стал пытаться это изменить.