(LINQを使用して)リストを取得し、それをリストのリストに分割して、8番目のエントリごとに元のリストを分割するにはどうすればよいですか?
このようなものにはスキップやテイクが含まれると思いますが、私はまだLINQを初めて使用します。
編集:C#/ .Net 3.5を使用する
Edit2:この質問は、他の「重複する」質問とは言い方が異なります。問題は似ていますが、この質問の回答は優れています。「受け入れられた」回答は(yield
ステートメントを使用して)非常にしっかりしています。 「その他」の質問。)重複は、問題の再調査を強いるという点で良い場合があります。
次の拡張メソッドを使用して、入力をサブセットに分割します
public static class IEnumerableExtensions
{
public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
{
List<T> toReturn = new List<T>(max);
foreach(var item in source)
{
toReturn.Add(item);
if (toReturn.Count == max)
{
yield return toReturn;
toReturn = new List<T>(max);
}
}
if (toReturn.Any())
{
yield return toReturn;
}
}
}
MoreLinq のようなライブラリを使用したほうがよいですが、「プレーンなLINQ」を使用してこれを行う必要がある場合は、GroupBy
を使用できます。 :
_var sequence = new[] {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
var result = sequence.Select((x, i) => new {Group = i/8, Value = x})
.GroupBy(item => item.Group, g => g.Value)
.Select(g => g.Where(x => true));
// result is: { {1,2,3,4,5,6,7,8}, {9,10,11,12,13,14,15,16} }
_
基本的に、消費される値のインデックスを提供するSelect()
のバージョンを使用します。インデックスを8で除算して、各値が属するグループを識別します。次に、このグループ化キーでシーケンスをグループ化します。最後のSelect
は、_IGrouping<>
_を_IEnumerable<IEnumerable<T>>
_に減らすだけです(IGrouping
はIEnumerable
であるため、厳密には必要ありません)。
例の定数_8
_を因数分解し、それを指定されたパラメーターに置き換えることで、これを再利用可能なメソッドに変換するのは簡単です。これは必ずしも最もエレガントなソリューションであるとは限らず、もはや遅延したストリーミングソリューションではありません...しかし、機能します。
イテレータブロック(_yield return
_)を使用して独自の拡張メソッドを作成することもできます。これにより、パフォーマンスが向上し、GroupBy
より少ないメモリを使用できます。これは、MoreLinqのBatch()
メソッドがIIRCを実行するものです。
これは、元のLinqデザイナーが考えていたものとはまったく異なりますが、GroupByのこの誤用を確認してください。
public static IEnumerable<IEnumerable<T>> BatchBy<T>(this IEnumerable<T> items, int batchSize)
{
var count = 0;
return items.GroupBy(x => (count++ / batchSize)).ToList();
}
[TestMethod]
public void BatchBy_breaks_a_list_into_chunks()
{
var values = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var batches = values.BatchBy(3);
batches.Count().ShouldEqual(4);
batches.First().Count().ShouldEqual(3);
batches.Last().Count().ShouldEqual(1);
}
この質問には「ゴルフ」賞を受賞したと思います。 ToList
は非常に重要です。出力で何かを実行する前に、グループ化が実際に実行されていることを確認したいからです。 ToList
を削除すると、奇妙な副作用が発生します。
Takeは、取得したエントリを削除しないので、あまり効率的ではありません。
なぜ単純なループを使わないのですか:
public IEnumerable<IList<T>> Partition<T>(this/* <-- see extension methods*/ IEnumerable<T> src,int num)
{
IEnumerator<T> enu=src.getEnumerator();
while(true)
{
List<T> result=new List<T>(num);
for(int i=0;i<num;i++)
{
if(!enu.MoveNext())
{
if(i>0)yield return result;
yield break;
}
result.Add(enu.Current);
}
yield return result;
}
}
from b in Enumerable.Range(0,8) select items.Where((x,i) => (i % 8) == b);
最も簡単な解決策はMelによって与えられます:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items,
int partitionSize)
{
int i = 0;
return items.GroupBy(x => i++ / partitionSize).ToArray();
}
簡潔だが遅い。上記のメソッドは、IEnumerableを必要な固定サイズのチャンクに分割し、チャンクの総数は重要ではありません。 IEnumerableを等しいサイズまたは同じサイズに近いN個のチャンクに分割するには、次のようにします。
public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> items,
int numOfParts)
{
int i = 0;
return items.GroupBy(x => i++ % numOfParts);
}
物事をスピードアップするには、簡単なアプローチが行います:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items,
int partitionSize)
{
if (partitionSize <= 0)
throw new ArgumentOutOfRangeException("partitionSize");
int innerListCounter = 0;
int numberOfPackets = 0;
foreach (var item in items)
{
innerListCounter++;
if (innerListCounter == partitionSize)
{
yield return items.Skip(numberOfPackets * partitionSize).Take(partitionSize);
innerListCounter = 0;
numberOfPackets++;
}
}
if (innerListCounter > 0)
yield return items.Skip(numberOfPackets * partitionSize);
}
これは、現在地球上にあるものよりも高速です:) Split
操作の同等のメソッド here