2 つの配列を一度に反復するにはどうすればよいですか?
-
20-08-2019 - |
質問
テキスト ファイルの解析中に 2 つの配列が構築されました。最初の列には列名が含まれ、2 番目の列には現在の行の値が含まれます。マップを構築するには、両方のリストを一度に反復処理する必要があります。現在、私には次のようなものがあります。
var currentValues = currentRow.Split(separatorChar);
var valueEnumerator = currentValues.GetEnumerator();
foreach (String column in columnList)
{
valueEnumerator.MoveNext();
valueMap.Add(column, (String)valueEnumerator.Current);
}
これは問題なく機能しますが、私の優雅さの感覚を完全に満たすものではなく、配列の数が 2 つよりも大きい場合 (私は時々そうする必要がありますが) 非常に面倒になります。他に簡潔なイディオムを持っている人はいますか?
解決
列名の同じ番号が存在する場合、各行の要素があるとして、あなたはループのために使うことができませんでした?
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
}
あるいは、汎用のペア タイプを作成し、PairUp メソッドの射影パラメータを削除することもできます。
編集:
ペア タイプの場合、呼び出しコードは次のようになります。
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 の一部になることが期待されます。 バート・デ・スメット は、既存の 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});
、最良の方法は、単にインデックスを持つ従来のfor
ループを使用することが考えられます。ない許可されますが、私の知る限りでは、.NETは、これを行うためのより良い方法を提供していません。、など素敵な
またzip
と呼ばれるメソッドにコードをカプセル化することができ - これは一般的な高次リスト機能です。しかしながら、適切なタプル型を欠くC#が、これはかなりcruftyです。あなたはとても素敵ではありません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);
}
の代わりに2つの別々の配列を作成するあなたは、二次元配列、または(より良いだろう)辞書を作ることができます。それが動作するかどうかしかし、実際に、私はそれを変更しようとしないでしょう。