是否有任何方法可以将List<SomeObject>分离为SomeObject的几个单独的列表,使用项目索引作为每个分割的分隔符?

让我举个例子:

我有一个List<SomeObject>,我需要一个List<List<SomeObject>>或List<SomeObject>[],这样每个结果列表将包含一组原始列表的3个项目(依次)。

eg.:

原始列表:[a, g, e, w, p, s, q, f, x, y, i, m, c] 结果列表:[a、g e], [w、p, s], [q, f, x]、[y,我,m], [c]

我还需要结果列表的大小是这个函数的参数。


当前回答

另一种方法是使用Rx Buffer操作符

//using System.Linq;
//using System.Reactive.Linq;
//using System.Reactive.Threading.Tasks;

var observableBatches = anAnumerable.ToObservable().Buffer(size);

var batches = aList.ToObservable().Buffer(size).ToList().ToTask().GetAwaiter().GetResult();

其他回答

您可以使用一些使用Take和Skip的查询,但我认为这会在原始列表上增加太多迭代。

相反,我认为你应该创建一个自己的迭代器,如下所示:

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的回答,我觉得有一个更简单的方法:

再次遍历列表(我最初没有这样做) 在释放块之前将项目物化到组中(对于大块的项目,将会有内存问题) 山姆发布的所有代码

也就是说,这里是另一个传递,我已经在一个扩展方法中编码为IEnumerable<T>,称为Chunk:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
    int chunkSize)
{
    // Validate parameters.
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (chunkSize <= 0) throw new ArgumentOutOfRangeException(nameof(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>上被调用,它产生Current返回的项,然后增加计数,确保永远不会返回超过chunkSize的项,并在每次迭代后移动到序列中的下一个项(但如果产生的项的数量超过块大小,则会短路)。

如果没有剩余的项目,那么InternalChunk方法将在外层循环中进行另一次传递,但当MoveNext第二次被调用时,它仍然会返回false,正如文档所述(强调我的):

如果MoveNext经过集合的末尾,则枚举数为 定位在集合和MoveNext的最后一个元素之后 返回false。当枚举器位于此位置时,执行后续操作 调用MoveNext也返回false,直到调用Reset。

此时,循环将中断,序列的序列将终止。

这是一个简单的测试:

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,

一个重要的注意事项是,如果不耗尽整个子序列或在父序列的任何一点中断,这将不起作用。这是一个重要的警告,但是如果您的用例是您将使用序列的序列的每个元素,那么这将适合您。

此外,如果你改变顺序,它会做一些奇怪的事情,就像Sam曾经做的那样。

下面这个解是我能想到的最紧凑的解是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;
    }
}

如果源集合实现了IList < T >(按索引随机访问),我们可以使用下面的方法。它只在真正访问元素时获取元素,因此这对于延迟求值的集合特别有用。类似于unbounded IEnumerable< T >,但是对于IList< T >。

    public static IEnumerable<IEnumerable<T>> Chunkify<T>(this IList<T> src, int chunkSize)
    {
        if (src == null) throw new ArgumentNullException(nameof(src));
        if (chunkSize < 1) throw new ArgumentOutOfRangeException(nameof(chunkSize), $"must be > 0, got {chunkSize}");

        for(var ci = 0; ci <= src.Count/chunkSize; ci++){
            yield return Window(src, ci*chunkSize, Math.Min((ci+1)*chunkSize, src.Count)-1);
        }
    }

    private static IEnumerable<T> Window<T>(IList<T> src, int startIdx, int endIdx)
    {
        Console.WriteLine($"window {startIdx} - {endIdx}");
        while(startIdx <= endIdx){
            yield return src[startIdx++];
        }
    }

如果列表的类型为system.collections.generic,则可以使用“CopyTo”方法将数组中的元素复制到其他子数组中。您可以指定开始元素和要复制的元素数量。

你也可以对你的原始列表做3个克隆,并在每个列表上使用“RemoveRange”将列表缩小到你想要的大小。

或者只是创建一个helper方法来为您做这件事。

下面是我几个月前写的一个列表拆分例程:

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;
}