用LINQ将列表拆分成子列表
有没有什么办法可以将List<SomeObject>
分隔成几个单独的SomeObject
列表,使用item索引作为每个split的分隔符?
让我举个例子:
我有一个List<SomeObject>
,我需要一个List<List<SomeObject>>
或List<SomeObject>[]
,以便每个这样的结果列表将包含一组原始列表(按顺序)的3个项目。
例如。:
-
原始列表:
[a, g, e, w, p, s, q, f, x, y, i, m, c]
-
结果列表:
[a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]
我也需要生成的列表大小作为这个函数的参数。
尝试下面的代码。
public static IList<IList<T>> Split<T>(IList<T> source) { return source .Select((x, i) => new { Index = i, Value = x }) .GroupBy(x => x.Index / 3) .Select(x => x.Select(v => v.Value).ToList()) .ToList(); }
这个想法是首先按照索引对元素进行分组。 将它们分成三组,然后将每个组转换为一个列表,将列表的IEnumerable
转换为List
s
这个问题有点老了,但我只是写了这个,我认为它比其他build议的解决scheme更优雅一些:
/// <summary> /// Break a list of items into chunks of a specific size /// </summary> public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize) { while (source.Any()) { yield return source.Take(chunksize); source = source.Skip(chunksize); } }
一般来说, CaseyB提出的方法工作正常,事实上,如果你传入一个List<T>
,很难将其错误的,也许我会改变它:
public static IEnumerable<IEnumerable<T>> ChunkTrivialBetter<T>(this IEnumerable<T> source, int chunksize) { var pos = 0; while (source.Skip(pos).Any()) { yield return source.Skip(pos).Take(chunksize); pos += chunksize; } }
这将避免大规模的呼叫链。 尽pipe如此,这种方法有一个普遍的缺陷。 它实现了每个块的两个枚举,以突出问题尝试运行:
foreach (var item in Enumerable.Range(1, int.MaxValue).Chunk(8).Skip(100000).First()) { Console.WriteLine(item); } // wait forever
为了克服这个问题,我们可以尝试卡梅隆的方法,它通过上面的testing中的颜色,因为它只是一次枚举。
麻烦的是,它有一个不同的缺陷,它实现每个块中的每个项目,这种方法的麻烦是,你在内存上运行高。
为了说明这个尝试运行:
foreach (var item in Enumerable.Range(1, int.MaxValue) .Select(x => x + new string('x', 100000)) .Clump(10000).Skip(100).First()) { Console.Write('.'); } // OutOfMemoryException
最后,任何实现都应该能够处理块的乱序迭代,例如:
Enumerable.Range(1,3).Chunk(2).Reverse.ToArray() // should return [3],[1,2]
许多高度优化的解决scheme,比如我对这个答案的第一次修改 ,都没有成功 在casperOne的优化答案中可以看到同样的问题。
要解决所有这些问题,您可以使用以下内容:
namespace ChunkedEnumerator { public static class Extensions { class ChunkedEnumerable<T> : IEnumerable<T> { class ChildEnumerator : IEnumerator<T> { ChunkedEnumerable<T> parent; int position; bool done = false; T current; public ChildEnumerator(ChunkedEnumerable<T> parent) { this.parent = parent; position = -1; parent.wrapper.AddRef(); } public T Current { get { if (position == -1 || done) { throw new InvalidOperationException(); } return current; } } public void Dispose() { if (!done) { done = true; parent.wrapper.RemoveRef(); } } object System.Collections.IEnumerator.Current { get { return Current; } } public bool MoveNext() { position++; if (position + 1 > parent.chunkSize) { done = true; } if (!done) { done = !parent.wrapper.Get(position + parent.start, out current); } return !done; } public void Reset() { // per http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.reset.aspx throw new NotSupportedException(); } } EnumeratorWrapper<T> wrapper; int chunkSize; int start; public ChunkedEnumerable(EnumeratorWrapper<T> wrapper, int chunkSize, int start) { this.wrapper = wrapper; this.chunkSize = chunkSize; this.start = start; } public IEnumerator<T> GetEnumerator() { return new ChildEnumerator(this); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return GetEnumerator(); } } class EnumeratorWrapper<T> { public EnumeratorWrapper (IEnumerable<T> source) { SourceEumerable = source; } IEnumerable<T> SourceEumerable {get; set;} Enumeration currentEnumeration; class Enumeration { public IEnumerator<T> Source { get; set; } public int Position { get; set; } public bool AtEnd { get; set; } } public bool Get(int pos, out T item) { if (currentEnumeration != null && currentEnumeration.Position > pos) { currentEnumeration.Source.Dispose(); currentEnumeration = null; } if (currentEnumeration == null) { currentEnumeration = new Enumeration { Position = -1, Source = SourceEumerable.GetEnumerator(), AtEnd = false }; } item = default(T); if (currentEnumeration.AtEnd) { return false; } while(currentEnumeration.Position < pos) { currentEnumeration.AtEnd = !currentEnumeration.Source.MoveNext(); currentEnumeration.Position++; if (currentEnumeration.AtEnd) { return false; } } item = currentEnumeration.Source.Current; return true; } int refs = 0; // needed for dispose semantics public void AddRef() { refs++; } public void RemoveRef() { refs--; if (refs == 0 && currentEnumeration != null) { var copy = currentEnumeration; currentEnumeration = null; copy.Source.Dispose(); } } } public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize) { if (chunksize < 1) throw new InvalidOperationException(); var wrapper = new EnumeratorWrapper<T>(source); int currentPos = 0; T ignore; try { wrapper.AddRef(); while (wrapper.Get(currentPos, out ignore)) { yield return new ChunkedEnumerable<T>(wrapper, chunksize, currentPos); currentPos += chunksize; } } finally { wrapper.RemoveRef(); } } } class Program { static void Main(string[] args) { int i = 10; foreach (var group in Enumerable.Range(1, int.MaxValue).Skip(10000000).Chunk(3)) { foreach (var n in group) { Console.Write(n); Console.Write(" "); } Console.WriteLine(); if (i-- == 0) break; } var stuffs = Enumerable.Range(1, 10).Chunk(2).ToArray(); foreach (var idx in new [] {3,2,1}) { Console.Write("idx " + idx + " "); foreach (var n in stuffs[idx]) { Console.Write(n); Console.Write(" "); } Console.WriteLine(); } /* 10000001 10000002 10000003 10000004 10000005 10000006 10000007 10000008 10000009 10000010 10000011 10000012 10000013 10000014 10000015 10000016 10000017 10000018 10000019 10000020 10000021 10000022 10000023 10000024 10000025 10000026 10000027 10000028 10000029 10000030 10000031 10000032 10000033 idx 3 7 8 idx 2 5 6 idx 1 3 4 */ Console.ReadKey(); } } }
还有一些优化你可以引入块的无序迭代,这是超出范围。
至于你应该select哪种方法? 这完全取决于你正在努力解决的问题。 如果你不关心第一个缺陷,简单的答案是非常有吸引力的。
注意和大多数方法一样,这对于multithreading来说是不安全的,如果你希望使线程安全,那么你可能需要修改EnumeratorWrapper
。
你可以使用一些使用Take
和Skip
的查询,但是我相信这会在原始列表中增加太多的迭代。
相反,我想你应该创build一个你自己的迭代器,就像这样:
public static IEnumerable<IEnumerable<T>> GetEnumerableOfEnumerables<T>( IEnumerable<T> enumerable, int groupSize) { // The list to return. List<T> list = new List<T>(groupSize); // Cycle through all of the items. foreach (T item in enumerable) { // Add the item. list.Add(item); // If the list has the number of elements, return that. if (list.Count == groupSize) { // Return the list. yield return list; // Set the list to a new list. list = new List<T>(groupSize); } } // Return the remainder if there is any, if (list.Count != 0) { // Return the list. yield return list; } }
然后你可以调用它,并启用LINQ,这样你就可以对结果序列执行其他操作。
根据Sam的回答 ,我觉得有一个更简单的方法来做到这一点没有:
- 再次重复列表(我原本没有这样做)
- 释放组块之前将组中的项目实现化(对于大块项目,会有内存问题)
- 所有Sam发布的代码
这就是说,这是另一个通过,我已经编写了一个扩展方法IEnumerable<T>
称为Chunk
:
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunkSize) { // Validate parameters. if (source == null) throw new ArgumentNullException("source"); if (chunkSize <= 0) throw new ArgumentOutOfRangeException("chunkSize", "The chunkSize parameter must be a positive value."); // Call the internal implementation. return source.ChunkInternal(chunkSize); }
没有什么奇怪的,只是基本的错误检查。
转到ChunkInternal
:
private static IEnumerable<IEnumerable<T>> ChunkInternal<T>( this IEnumerable<T> source, int chunkSize) { // Validate parameters. Debug.Assert(source != null); Debug.Assert(chunkSize > 0); // Get the enumerator. Dispose of when done. using (IEnumerator<T> enumerator = source.GetEnumerator()) do { // Move to the next element. If there's nothing left // then get out. if (!enumerator.MoveNext()) yield break; // Return the chunked sequence. yield return ChunkSequence(enumerator, chunkSize); } while (true); }
基本上,它获取IEnumerator<T>
并手动遍历每个项目。 它检查是否有任何项目目前被列举。 在每个块被枚举后,如果没有剩下任何项目,就会发生。
一旦检测到序列中有项目,它将内部IEnumerable<T>
实现的责任委托给ChunkSequence
:
private static IEnumerable<T> ChunkSequence<T>(IEnumerator<T> enumerator, int chunkSize) { // Validate parameters. Debug.Assert(enumerator != null); Debug.Assert(chunkSize > 0); // The count. int count = 0; // There is at least one item. Yield and then continue. do { // Yield the item. yield return enumerator.Current; } while (++count < chunkSize && enumerator.MoveNext()); }
由于MoveNext
已经在传递给ChunkSequence
的IEnumerator<T>
上ChunkSequence
,它产生了Current
返回的项目,然后递增计数,确保不会返回比chunkSize
更多的项目,并在每次迭代之后移动到序列中的下一个项目但是如果产生的物品数量超过块大小,则会短路)。
如果没有剩余的项目,则InternalChunk
方法将在外部循环中再次进行传递,但是当第二次MoveNext
时,仍然会按照文档 (重点介绍)返回false。
如果MoveNext传递集合的末尾,则枚举器将位于集合中的最后一个元素之后,并且MoveNext返回false。 当枚举器在这个位置时,对MoveNext的后续调用也会返回false,直到调用Reset。
此时,循环将中断,并且序列的序列将终止。
这是一个简单的testing:
static void Main() { string s = "agewpsqfxyimc"; int count = 0; // Group by three. foreach (IEnumerable<char> g in s.Chunk(3)) { // Print out the group. Console.Write("Group: {0} - ", ++count); // Print the items. foreach (char c in g) { // Print the item. Console.Write(c + ", "); } // Finish the line. Console.WriteLine(); } }
输出:
Group: 1 - a, g, e, Group: 2 - w, p, s, Group: 3 - q, f, x, Group: 4 - y, i, m, Group: 5 - c,
一个重要的注意事项是,如果不排除整个子序列或在父序列的任何一点中断,这将不起作用。 这是一个重要的警告,但是如果你的用例是你会消耗顺序序列的每一个元素,那么这将为你工作。
另外,如果你玩这个命令,它会做一些奇怪的事情,就像山姆一样 。
好的,这是我的承诺:
- 完全懒惰:在无限的枚举上工作
- 没有中间复制/缓冲
- O(n)执行时间
- 当内部序列仅被部分消耗时也起作用
public static IEnumerable<IEnumerable<T>> Chunks<T>(this IEnumerable<T> enumerable, int chunkSize) { if (chunkSize < 1) throw new ArgumentException("chunkSize must be positive"); using (var e = enumerable.GetEnumerator()) while (e.MoveNext()) { var remaining = chunkSize; // elements remaining in the current chunk var innerMoveNext = new Func<bool>(() => --remaining > 0 && e.MoveNext()); yield return e.GetChunk(innerMoveNext); while (innerMoveNext()) {/* discard elements skipped by inner iterator */} } } private static IEnumerable<T> GetChunk<T>(this IEnumerator<T> e, Func<bool> innerMoveNext) { do yield return e.Current; while (innerMoveNext()); }
用法示例
var src = new [] {1, 2, 3, 4, 5, 6}; var c3 = src.Chunks(3); // {{1, 2, 3}, {4, 5, 6}}; var c4 = src.Chunks(4); // {{1, 2, 3, 4}, {5, 6}}; var sum = c3.Select(c => c.Sum()); // {6, 15} var count = c3.Count(); // 2 var take2 = c3.Select(c => c.Take(2)); // {{1, 2}, {4, 5}}
说明
代码通过嵌套两个基于yield
的迭代器来工作。
外部迭代器必须跟踪内部(块)迭代器有效消耗了多less元素。 这是通过closuresremaining
的innerMoveNext()
。 在下一个块由外部迭代器产生之前,块的未消耗的元素被丢弃。 这是必要的,否则你会得到不一致的结果,当内部枚举不是(完全)消耗(例如c3.Count()
将返回6)。
注: 答案已更新,以解决@aolszowka指出的缺点。
完全懒惰,不计数或复制:
public static class EnumerableExtensions { public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int len) { if (len == 0) throw new ArgumentNullException(); var enumer = source.GetEnumerator(); while (enumer.MoveNext()) { yield return Take(enumer.Current, enumer, len); } } private static IEnumerable<T> Take<T>(T head, IEnumerator<T> tail, int len) { while (true) { yield return head; if (--len == 0) break; if (tail.MoveNext()) head = tail.Current; else break; } } }
我认为以下build议将是最快的。 我为了使用Array.Copy的能力牺牲了源Enumerable的懒惰,并且知道了每个子列表的长度。
public static IEnumerable<T[]> Chunk<T>(this IEnumerable<T> items, int size) { T[] array = items as T[] ?? items.ToArray(); for (int i = 0; i < array.Length; i+=size) { T[] chunk = new T[Math.Min(size, array.Length - i)]; Array.Copy(array, i, chunk, 0, chunk.Length); yield return chunk; } }
我们可以改进@ JaredPar的解决scheme来做真正的懒惰评估。 我们使用一个GroupAdjacentBy
方法来产生具有相同键的连续元素组:
sequence .Select((x, i) => new { Value = x, Index = i }) .GroupAdjacentBy(x=>x.Index/3) .Select(g=>g.Select(x=>x.Value))
因为这些组是一个接一个地产生的,所以这个解决scheme可以有效地处理长序列或无限序列。
System.Interactive为此提供了Buffer()
。 一些快速testing显示,性能与Sam的解决scheme类似。
这是几个月前我写的一个列表拆分例程:
public static List<List<T>> Chunk<T>( List<T> theList, int chunkSize ) { List<List<T>> result = theList .Select((x, i) => new { data = x, indexgroup = i / chunkSize }) .GroupBy(x => x.indexgroup, x => x.data) .Select(g => new List<T>(g)) .ToList(); return result; }
几年前我写了一个Clump扩展方法。 伟大的工程,这是最快的实施。 :P
/// <summary> /// Clumps items into same size lots. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="source">The source list of items.</param> /// <param name="size">The maximum size of the clumps to make.</param> /// <returns>A list of list of items, where each list of items is no bigger than the size given.</returns> public static IEnumerable<IEnumerable<T>> Clump<T>(this IEnumerable<T> source, int size) { if (source == null) throw new ArgumentNullException("source"); if (size < 1) throw new ArgumentOutOfRangeException("size", "size must be greater than 0"); return ClumpIterator<T>(source, size); } private static IEnumerable<IEnumerable<T>> ClumpIterator<T>(IEnumerable<T> source, int size) { Debug.Assert(source != null, "source is null."); T[] items = new T[size]; int count = 0; foreach (var item in source) { items[count] = item; count++; if (count == size) { yield return items; items = new T[size]; count = 0; } } if (count > 0) { if (count == size) yield return items; else { T[] tempItems = new T[count]; Array.Copy(items, tempItems, count); yield return tempItems; } } }
这是一个古老的问题,但这是我结束了; 它枚举枚举只有一次,但确实为每个分区创build列表。 当ToArray()
被调用时,它不会遭受意外的行为,因为一些实现可以执行:
public static IEnumerable<IEnumerable<T>> Partition<T>(IEnumerable<T> source, int chunkSize) { if (source == null) { throw new ArgumentNullException("source"); } if (chunkSize < 1) { throw new ArgumentException("Invalid chunkSize: " + chunkSize); } using (IEnumerator<T> sourceEnumerator = source.GetEnumerator()) { IList<T> currentChunk = new List<T>(); while (sourceEnumerator.MoveNext()) { currentChunk.Add(sourceEnumerator.Current); if (currentChunk.Count == chunkSize) { yield return currentChunk; currentChunk = new List<T>(); } } if (currentChunk.Any()) { yield return currentChunk; } } }
我发现这个小片段很好地完成了这项工作。
public static IEnumerable<List<T>> Chunked<T>(this List<T> source, int chunkSize) { var offset = 0; while (offset < source.Count) { yield return source.GetRange(offset, Math.Min(source.Count - offset, chunkSize)); offset += chunkSize; } }
这下面的解决scheme是最紧凑,我可以拿出是O(n)。
public static IEnumerable<T[]> Chunk<T>(IEnumerable<T> source, int chunksize) { var list = source as IList<T> ?? source.ToList(); for (int start = 0; start < list.Count; start += chunksize) { T[] chunk = new T[Math.Min(chunksize, list.Count - start)]; for (int i = 0; i < chunk.Length; i++) chunk[i] = list[start + i]; yield return chunk; } }
旧的代码,但这是我一直在使用:
public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max) { var 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; } }
如果列表的types是system.collections.generic,则可以使用“CopyTo”方法将数组的元素复制到其他子数组中。 您指定要复制的开始元素和元素数量。
您也可以创build3个克隆的原始列表,并使用每个列表上的“RemoveRange”将列表缩小到您想要的大小。
或者只是创build一个帮手的方法来为你做。
我们发现David B的解决scheme效果最好。 但是我们将其调整为更一般的解决scheme:
list.GroupBy(item => item.SomeProperty) .Select(group => new List<T>(group)) .ToArray();
这个如何?
var input = new List<string> { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" }; var k = 3 var res = Enumerable.Range(0, (input.Count - 1) / k + 1) .Select(i => input.GetRange(i * k, Math.Min(k, input.Count - i * k))) .ToList();
据我所知, GetRange()是线性的项目数量。 所以这应该performance良好。
使用模块化分区:
public IEnumerable<IEnumerable<string>> Split(IEnumerable<string> input, int chunkSize) { var chunks = (int)Math.Ceiling((double)input.Count() / (double)chunkSize); return Enumerable.Range(0, chunks).Select(id => input.Where(s => s.GetHashCode() % chunks == id)); }
只要把我的两分钱。 如果您想“列出”列表(从左到右可视化),则可以执行以下操作:
public static List<List<T>> Buckets<T>(this List<T> source, int numberOfBuckets) { List<List<T>> result = new List<List<T>>(); for (int i = 0; i < numberOfBuckets; i++) { result.Add(new List<T>()); } int count = 0; while (count < source.Count()) { var mod = count % numberOfBuckets; result[mod].Add(source[count]); count++; } return result; }
这是一个旧的解决scheme,但我有一个不同的方法。 我使用Skip
来移动到所需的偏移量,并Take
提取所需的元素数量:
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunkSize) { if (chunkSize <= 0) throw new ArgumentOutOfRangeException($"{nameof(chunkSize)} should be > 0"); var nbChunks = (int)Math.Ceiling((double)source.Count()/chunkSize); return Enumerable.Range(0, nbChunks) .Select(chunkNb => source.Skip(chunkNb*chunkSize) .Take(chunkSize)); }
我采取了主要的答案,并将其作为一个国际奥委会容器,以确定在哪里拆分。 ( 对于谁真的只想分裂3个项目,在阅读这个职位,同时寻找答案? )
这种方法允许根据需要在任何types的项目上进行分割。
public static List<List<T>> SplitOn<T>(List<T> main, Func<T, bool> splitOn) { int groupIndex = 0; return main.Select( item => new { Group = (splitOn.Invoke(item) ? ++groupIndex : groupIndex), Value = item }) .GroupBy( it2 => it2.Group) .Select(x => x.Select(v => v.Value).ToList()) .ToList(); }
所以对于OP的代码将是
var it = new List<string>() { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" }; int index = 0; var result = SplitOn(it, (itm) => (index++ % 3) == 0 );
插入我的两分钱…
By using the list type for the source to be chunked, I found another very compact solution:
public static IEnumerable<IEnumerable<TSource>> Chunk<TSource>(this IEnumerable<TSource> source, int chunkSize) { // copy the source into a list var chunkList = source.ToList(); // return chunks of 'chunkSize' items while (chunkList.Count > chunkSize) { yield return chunkList.GetRange(0, chunkSize); chunkList.RemoveRange(0, chunkSize); } // return the rest yield return chunkList; }