【问题标题】:LINQ Partition List into Lists of 8 members [duplicate]LINQ分区列表到8个成员列表中[重复]
【发布时间】:2011-04-15 23:06:03
【问题描述】:

如何获取一个列表(使用 LINQ)并将其分解为一个列表列表,每 8 个条目对原始列表进行分区?

我想这样的事情会涉及 Skip 和/或 Take,但我对 LINQ 还是很陌生。

编辑:使用 C# / .Net 3.5

Edit2:这个问题的措辞与其他“重复”问题不同。尽管问题相似,但这个问题的答案更胜一筹:“接受”的答案非常可靠(带有yield 声明)以及 Jon Skeet 建议使用 MoreLinq(在“其他”中不推荐)问题。)有时重复是好的,因为它们会强制重新检查问题。

【问题讨论】:

  • 您使用的是 VB 还是 C#?迭代器的存在有很大的不同。
  • 这不是重复的。另一个问题想要将列表分解为每个第 n 个元素的子列表,因此包含元素 0、8、16、24 等的列表和包含元素 1、9、17、25 等的列表和列表包含元素 2、10、18 等。这个用户想要分成一个 0..7 的列表、一个 8..15 的列表和一个 16..24 的列表,类似于分页

标签: linq partitioning skip take


【解决方案1】:

使用以下扩展方法将输入分解为子集

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

【讨论】:

  • 我现在要试试这个,因为这看起来很聪明......“收益回报”的想法突然出现在我的脑海中,同时考虑了这个问题,但我找不到明确的方法做吧...我会让你知道这对我有什么作用。
  • 哇!这真是太酷了。我要和这个一起去!谢谢您的帮助! :-)
【解决方案2】:

我们在MoreLINQ 中有这样一个方法,即Batch 方法:

// As IEnumerable<IEnumerable<T>>
var items = list.Batch(8);

// As IEnumerable<List<T>>
var items = list.Batch(8, seq => seq.ToList());

【讨论】:

  • 酷,这实现得非常好,包括一个 resultSelector(对于操作/排序内部列表很重要)。
  • 唷。我想也许我无法弄清楚这一点有点愚蠢。很高兴看到常规 LINQ-to-Objects 中“缺少”一些东西。 :)
  • @Pretzel:这并不是说使用普通的旧 LINQ 是不可能的……只是它既不是非常有效也不是很容易理解。请参阅我对“普通 LINQ”示例的回答。
  • +1,感谢您提供此库的链接。我会看看我是否可以在以后的项目中使用它。
  • 小问题 - 我碰巧复制了 Batch 代码,Resharper 警告我在 return Batch(source, size, x =&gt; x);x 可能是多重枚举问题。正确/忽略?
【解决方案3】:

你最好使用像MoreLinq 这样的库,但如果你真的必须使用“plain 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&lt;&gt; 减少到IEnumerable&lt;IEnumerable&lt;T&gt;&gt;(并且不是绝对必要的,因为IGroupingIEnumerable)。

通过分解示例中的常量8 并将其替换为指定的参数,很容易将其转换为可重用的方法。 它不一定是最优雅的解决方案,也不再是一种懒惰的流式解决方案......但它确实有效。

您还可以使用迭代器块 (yield return) 编写自己的扩展方法,这可以为您提供比 GroupBy 更好的性能和使用更少的内存。这就是 MoreLinq 的 Batch() 方法所做的 IIRC。

【讨论】:

  • 感谢您的意见。是的,它似乎效率不高,正如您可以想象的那样,我一直在努力理解如何使用常规 LINQ 来做到这一点。 (我现在正盯着你的答案,我真的不是很明白。)我以后得再摆弄它。 (再次感谢!)
  • 如果您计划批处理的序列将非常大(或无限),则使用 GroupBy() 的方法会失效。至于它是如何工作的 - 它创建一个匿名对象,将每个项目与其索引相关联,然后根据8(或任何其他非零常数)的可分性将其分组为一系列序列。
【解决方案4】:

这根本不是最初的 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,你会得到一些奇怪的副作用。

【讨论】:

  • 郑重声明,Handcraftsman 基于“收益回报”的版本性能要好得多,但我仍然喜欢这段代码的“嘿,你不应该那样做”方面。
  • -1 这个答案是错误的。如果你用模运算符替换除法,你会得到更适合这个线程的 batchSize 个分区 stackoverflow.com/questions/438188/…
  • 我不同意。如果您使用模数,那么您将按余数将项目分配给组。您将在第 0 组中获得第 0 项,在第 1 组中获得第 1 项,等等。这将完全打乱他们的订单。正如我在这里所做的那样,使用整数除法意味着项目 0-2 进入第 0 组,3-5 进入第 1 组,等等。我相信这是预期的。如果您同意,请从我的回答中删除 -1。
  • 你是对的。我错过了在离开之前实际执行查询的 ToList 部分。为什么不使用更轻的 ToArray?我必须编辑您的答案以删除我的 -1。也+1! :)
  • ToList 正是我当时使用的,但 ToArray 应该可以正常工作。
【解决方案5】:

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

【讨论】:

    【解决方案6】:
    from b in Enumerable.Range(0,8) select items.Where((x,i) => (i % 8) == b);
    

    【讨论】:

    • +1 但请注意此分组每 8 个项目,即您得到 {0,8,16}, {1,9,17}, ...
    • 这更多是关于拆分而不是分区 - 更适合here
    • 此外,如果数字 8 大于 items 中的项目数,这可能会产生空的内部 IEnumerables,这可能是可取的,也可能不是。无论如何,我将您的答案合并到另一个线程中..
    【解决方案7】:

    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

    【讨论】:

    • 唷,这要快得多!虽然stackoverflow.com/a/4835541/1037948 在 linqpad 中跑了几次之后开始让你脱颖而出...... ;)
    • @drzaus 请记住,该答案是具有副作用的危险结构。由于内部和外部在同一个枚举器上运行,因此您会得到意想不到的结果。如果仅枚举外部序列,则内部序列不再有效;或者您不能两次迭代内部序列。或者干脆试试var x = returnedResult.ElementAt(n).ToList();,你会得到意想不到的结果。
    • 所以我最近尝试再次运行一些性能比较 (tests+results here),在调用这个方法和对结果做一些事情之间,它与其他一些建议相比并没有那么快。我错过了什么吗?
    • @drzaus 你做了那个基准测试真是太好了。我会在一周内看到你的链接。我现在需要一些时间。谢谢 :) 会让你知道的。
    • 最近意识到“直截了当”的解决方案会导致重复枚举/评估 - 请参阅解释 gist.github.com/zaus/4cae1a5b42475a083287,这可能也在“已回答”问题中指出
    猜你喜欢
    • 2021-02-10
    • 2021-04-01
    • 2013-04-15
    • 2020-09-06
    • 2015-09-08
    • 2014-04-11
    • 2018-01-05
    • 2021-10-10
    • 2020-09-15
    相关资源
    最近更新 更多