我试图将一个列表拆分为一系列较小的列表。

我的问题:我的拆分列表功能没有将它们拆分为正确大小的列表。它应该把它们分成大小为30的列表,但它却把它们分成大小为114的列表?

如何让我的函数将一个列表分割成X个大小为30或更小的列表?

public static List<List<float[]>> splitList(List <float[]> locations, int nSize=30) 
{       
    List<List<float[]>> list = new List<List<float[]>>();

    for (int i=(int)(Math.Ceiling((decimal)(locations.Count/nSize))); i>=0; i--) {
        List <float[]> subLocat = new List <float[]>(locations); 

        if (subLocat.Count >= ((i*nSize)+nSize))
            subLocat.RemoveRange(i*nSize, nSize);
        else subLocat.RemoveRange(i*nSize, subLocat.Count-(i*nSize));

        Debug.Log ("Index: "+i.ToString()+", Size: "+subLocat.Count.ToString());
        list.Add (subLocat);
    }

    return list;
}

如果我在144大小的列表上使用该函数,那么输出是:

指数:4,尺寸:120 指数:3,尺寸:114 指数:2,尺寸:114 指数:1,尺寸:114 索引:0,大小:114


当前回答

我建议使用这个扩展方法按指定的块大小将源列表块到子列表:

/// <summary>
/// Helper methods for the lists.
/// </summary>
public static class ListExtensions
{
    public static List<List<T>> ChunkBy<T>(this List<T> source, int chunkSize) 
    {
        return source
            .Select((x, i) => new { Index = i, Value = x })
            .GroupBy(x => x.Index / chunkSize)
            .Select(x => x.Select(v => v.Value).ToList())
            .ToList();
    }
}

例如,如果你把18个项目分成5个块,它会给你一个包含4个子列表的列表,其中包含以下项目:5-5-5-3。

注意:在。net 6中即将对LINQ进行的分块改进 会像这样从盒子里出来:

const int PAGE_SIZE = 5;

IEnumerable<Movie[]> chunks = movies.Chunk(PAGE_SIZE);

其他回答

你可以只使用LINQ简单地尝试以下代码:

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

多一个

public static IList<IList<T>> SplitList<T>(this IList<T> list, int chunkSize)
{
    var chunks = new List<IList<T>>();
    List<T> chunk = null;
    for (var i = 0; i < list.Count; i++)
    {
        if (i % chunkSize == 0)
        {
            chunk = new List<T>(chunkSize);
            chunks.Add(chunk);
        }
        chunk.Add(list[i]);
    }
    return chunks;
}
public static List<List<float[]>> SplitList(List<float[]> locations, int nSize=30)  
{        
    var list = new List<List<float[]>>(); 

    for (int i = 0; i < locations.Count; i += nSize) 
    { 
        list.Add(locations.GetRange(i, Math.Min(nSize, locations.Count - i))); 
    } 

    return list; 
} 

通用版:

public static IEnumerable<List<T>> SplitList<T>(List<T> locations, int nSize=30)  
{        
    for (int i = 0; i < locations.Count; i += nSize) 
    { 
        yield return locations.GetRange(i, Math.Min(nSize, locations.Count - i)); 
    }  
} 

补充后很有用的评论mhand在最后

原来的答案

虽然大多数解决方案可能有效,但我认为它们不是很有效。假设你只想要前几个块中的前几项。这样你就不会想要遍历序列中的所有(无数)项。

下面将最多枚举两次:一次用于Take,一次用于Skip。它不会枚举比你使用的更多的元素:

public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>
    (this IEnumerable<TSource> source, int chunkSize)
{
    while (source.Any())                     // while there are elements left
    {   // still something to chunk:
        yield return source.Take(chunkSize); // return a chunk of chunkSize
        source = source.Skip(chunkSize);     // skip the returned chunk
    }
}

这将枚举序列多少次?

假设您将源代码划分为chunkSize的多个块。只枚举前N个块。对于每个枚举块,只枚举前M个元素。

While(source.Any())
{
     ...
}

Any将获得枚举器,执行1 MoveNext()并在处置枚举器后返回返回值。这样做N次

yield return source.Take(chunkSize);

根据参考来源,这将做如下的事情:

public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count)
{
    return TakeIterator<TSource>(source, count);
}

static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
    foreach (TSource element in source)
    {
        yield return element;
        if (--count == 0) break;
    }
}

在开始枚举所获取的Chunk之前,这不会做很多工作。如果您获取了几个Chunk,但决定不对第一个Chunk进行枚举,则不会执行foreach,正如调试器将显示的那样。

如果你决定取第一个块的前M个元素,那么yield返回将执行M次。这意味着:

获取枚举器 调用MoveNext()和Current M次。 处理枚举数

在第一个区块已经返回yield后,我们跳过第一个区块:

source = source.Skip(chunkSize);

同样,我们将查看参考源来找到skipiterator

static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
    using (IEnumerator<TSource> e = source.GetEnumerator()) 
    {
        while (count > 0 && e.MoveNext()) count--;
        if (count <= 0) 
        {
            while (e.MoveNext()) yield return e.Current;
        }
    }
}

如您所见,SkipIterator为Chunk中的每个元素调用一次MoveNext()。它不调用Current。

所以在每个Chunk中,我们看到完成了以下工作:

任何():GetEnumerator;1 MoveNext ();处理枚举器; 带(): 如果块的内容没有被枚举,则什么都没有。 如果内容被枚举:GetEnumerator(),每个枚举项一个MoveNext和一个Current, Dispose枚举器; Skip():对于每个被枚举的块(不是块的内容): GetEnumerator(), MoveNext() chunkSize times, no Current!处理枚举器

如果您观察枚举器发生了什么,您将看到有很多对MoveNext()的调用,而对于您实际决定访问的TSource项,只有对Current的调用。

如果你取N个大小为chunkSize的chunk,然后调用MoveNext()

Any() N次 只要你不枚举块,Take就没有时间了 N倍chunkSize for Skip()

如果您决定只枚举每个获取块的前M个元素,那么您需要为每个枚举块调用MoveNext M次。

MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)

所以如果你决定枚举所有块的所有元素:

MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once

MoveNext是否需要大量的工作,取决于源序列的类型。对于列表和数组,它是一个简单的索引增量,可能有一个超出范围的检查。

但是如果你的IEnumerable是数据库查询的结果,请确保该数据在你的计算机上是真正物化的,否则该数据将被多次获取。DbContext和Dapper会在访问数据之前正确地将数据传输到本地进程。如果你多次枚举相同的序列,它不会被多次获取。Dapper返回一个List对象,DbContext记住数据已经被获取。

在开始将项划分为块之前,调用AsEnumerable()或ToLists()是否明智取决于您的存储库

这个怎么样?我们的想法是只使用一个循环。谁知道呢,也许你在代码中只使用了IList实现,你不想强制转换为List。

private IEnumerable<IList<T>> SplitList<T>(IList<T> list, int totalChunks)
{
    IList<T> auxList = new List<T>();
    int totalItems = list.Count();

    if (totalChunks <= 0)
    {
        yield return auxList;
    }
    else 
    {
        for (int i = 0; i < totalItems; i++)
        {               
            auxList.Add(list[i]);           

            if ((i + 1) % totalChunks == 0)
            {
                yield return auxList;
                auxList = new List<T>();                
            }

            else if (i == totalItems - 1)
            {
                yield return auxList;
            }
        }
    }   
}