在 C# 中将多个 IEnumerable<T> 提要混合为一个

Posted

技术标签:

【中文标题】在 C# 中将多个 IEnumerable<T> 提要混合为一个【英文标题】:Mix several IEnumerable<T> feeds into one in C# 【发布时间】:2014-10-16 07:35:49 【问题描述】:

我经常需要在有IEnumerable&lt;T&gt; AIEnumerable&lt;T&gt; B 的地方执行操作,并且我想创建IEnumerable&lt;T&gt; C,其中 C 将从 A 获取 2T,然后从 B 获取 1T,然后再次从 A 获取 2T,然后从 B 获取 1T等等 - AABAABAAB...

C# 中是否有一些语言结构,也许是一些 LINQ 表达式可以轻松完成。

现在我编写了一个小助手类,使我能够做到这一点:

var C = EnumerableMixer(new int[]2, 1, A, B):

    public class EnumerableMixer<T> : IEnumerable<T>
    
        int[] Quantity;
        IEnumerable<T>[] Args;

        public EnumerableMixer(int[] quantity, params IEnumerable<T>[] args)
        
            this.Quantity = quantity;
            this.Args = args;
            if (quantity.Length != args.Length)
                throw new NotImplementedException("Quantity must have same length as number of args!");
        

        IEnumerable<T> Mix
        
            get
            
                var available = new List<int>(Quantity.Length);
                var enumerators = new List<IEnumerator<T>>(Quantity.Length);
                for (int i = 0; i < Quantity.Length; ++i)
                
                    available.Add(i);
                    enumerators.Add(Args[i].GetEnumerator());
                
                while (available.Count > 0)
                
                    for (int i = 0; i < available.Count; ++i)
                    
                        var id = available[i];
                        for (int j = 0; j < Quantity[id]; ++j)
                        
                            if (enumerators[id].MoveNext())
                                yield return enumerators[id].Current;
                            else
                            
                                available.RemoveAt(i);
                                i--;
                                break;
                            
                        
                    
                
            
        

        #region IEnumerable<T> Members

        public IEnumerator<T> GetEnumerator()
        
            return Mix.GetEnumerator();
        

        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        
            return this.GetEnumerator();
        

        #endregion
    

【问题讨论】:

如果一个枚举在另一个枚举之前完全枚举,会发生什么?例如:A= 1,4,8,9,B=0,1,2,3,4,5,6。应该是 1,4,0​​,8,9,1,2,3,4,5,6 还是 1,4,0​​,8,9,1 甚至是 1,4,0​​,8 ,9 还是抛出异常? 如果一个枚举不包含混合数量的多个元素倍数会发生什么?例如:A= 1,4,8,B=0,1,2,3,4,5,6。应该是 1,4,0​​,8,1,2,3,4,5,6 还是 1,4,0​​,8 还是抛出异常? 你知道Reactive Extensions吗?您谈论混合各种“提要”的方式对我来说感觉如此 Rx。在 Rx 中,有各种运算符可以将 IObservable 序列无缝地组合在一起,这对您来说真的很有用。我个人会认真考虑你的情况。 @CédricBignon 你的问题很好,我认为对于一般解决方案,我们必须有几个选项来处理枚举结束的枚举。 @julealgon 我认为 rx 在这里没有太大帮助 【参考方案1】:

版本 1

这是基于 adhie 的解决方案。它会考虑该比率并在退出该方法之前完全完成这两个序列。 flag 用于跟踪最后一个循环是否产生了任何东西。当flag 为假时,两个序列都被耗尽。

public IEnumerable<T> Mix<T>(IEnumerable<T> sequenceA, IEnumerable<T> sequenceB, int ratioA, int ratioB)

    var etorA = sequenceA.GetEnumerator();
    var etorB = sequenceB.GetEnumerator();
    bool flag = true;

    while(flag)
    
        flag = false;

        for(int i = 0; i < ratioA && (flag |= etorA.MoveNext()); i++)
            yield return etorA.Current;

        for(int i = 0; i < ratioB && (flag |= etorB.MoveNext()); i++)
            yield return etorB.Current;
    

版本 2

与版本 1 几乎相同,但采用可变数量的序列。

public IEnumerable<T> Mix<T>(params KeyValuePair<IEnumerable<T>, uint>[] quantifiedSequences)

    var sequences = quantifiedSequences.Select(x => new  Etor = x.Key.GetEnumerator(), Quantity = x.Value );
    bool flag = true;

    while(flag)
    
        foreach (var sequence in sequences)
            for(int i = 0; i < sequence.Quantity && (flag |= sequence.Etor.MoveNext()); i++)
                yield return sequence.Etor.Current;
    

【讨论】:

没有必要使用flag |= etorA.MoveNext()。你可以写etorA.MoveNext() 您需要跟踪最后一个循环是否返回任何内容,否则我们无法确定两个序列何时完成以退出 while 循环。 当 flag 初始化为 true 时,flag |= x 将始终为 true。修复了次要标志问题 = 清晰优雅的解决方案。【参考方案2】:

稍长一点的解决方案是:

    public IEnumerable<TValue> Mix<TValue>(IEnumerable<TValue> a, IEnumerable<TValue> b)
    
        var aEnumerator = a.GetEnumerator();
        var bEnumerator = b.GetEnumerator();

        while (true)
        
            if (!aEnumerator.MoveNext())
            
                yield break;
            

            yield return aEnumerator.Current;

            if (!aEnumerator.MoveNext())
            
                yield break;
            

            yield return aEnumerator.Current;

            if (!bEnumerator.MoveNext())
            
                yield break;
            

            yield return bEnumerator.Current;
        
    

此解决方案不会创建不必要的对象。如果你对 yield 关键字不熟悉:http://msdn.microsoft.com/en-us/library/9k7k7cf0.aspx

【讨论】:

不知道为什么这被否决(除非我错过了什么),这是我在看到 OP 实现了一个更复杂的版本之前要建议的。 我没有回答你的答案,但它没有选择将 2-1 比率设置为函数参数,这就是它更简单的原因。当其中一个完成时它也会中断,而我的将完成所有这些,不确定哪个更好。请注意,我的解决方案支持超过 2 个可枚举的组合。 @watbywbarif 那么你应该在你的要求中解释这一点,而不是仅仅说你想要有一个枚举,其中一个为每个对象迭代 2 个对象。 对不起,但这只是一个例子,如果你看问题,它会清楚地说明几个,而且从我的助手类中你可以看到我启用了几个枚举和每个自定义的样本数量。跨度> 我也没有说这个解决方案不好,它适用于我的情况,但最后当你比较两个解决方案时,如果两者都正常工作,你会采取一些额外的考虑喜欢哪个会表现得更好,哪个更通用,或者......我同意这里不需要像我在我的解决方案中那样使用 OP,因为静态函数会表现得更好,代码更短更简单,但这很容易改变。【参考方案3】:

这是我的 Linq 解决方案:

为每个元素分配一个索引,然后以正确的顺序获取元素。

// for example. You can use with any type
IEnumerable<Int32> listA = new List<Int32>()  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 ;
IEnumerable<Int32> listB = new List<Int32>()  101, 102, 103, 104, 105, 106, 107, 108, 109, 110;

// you want 2 of list A and 1 of list B. You can change the values
Int16[] quantity = new Int16[]  2, 1 ;

IEnumerable<Int32> listC = listA.Select((e, i) => new   element = e, index = (i / (quantity[0]) * (quantity[0] + quantity[1]) + (i%quantity[0])))
                                .Concat(listB.Select((e, i) => new   element = e, index = (i / (quantity[1]) * (quantity[0] + quantity[1]) + (i%quantity[1]) + quantity[0])))
                                .OrderBy(e => e.index)
                                .Select(e => e.element);

结果是IEnumerable&lt;Int32&gt;:1、2、101、3、4、102、5、6、103、7、8、104 ...

困难的是确定索引。它适用于您想要的(2,1、3,2、...)

对于第一个列表:

index = i/(quantity[0]) * (quantity[0]+quantity[1]) + (i%quantity[0]) + 0

对于第二个列表:

index = i/(quantity[1]) * (quantity[0]+quantity[1]) + (i%quantity[1]) + quantity[0]

【讨论】:

很酷的索引转换!就可用性而言,混合两个以上的可枚举项是不够通用的。无限枚举的问题。我没有dw。

以上是关于在 C# 中将多个 IEnumerable<T> 提要混合为一个的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 中将 IEnumerable<T> 转换为 List<T>?

在 c# 中将 WhereIf 用于多个条件

C# 索引器,实现IEnumerable接口的GetEnumerator()方法

在 ASP.NET Core 2.0 中将 DataTable 转换为 IEnumerable<T>

如何在 LINQ 中将 Group By 与 Task<IEnumerable<Entity>> 一起使用

C# LINQ 优雅地将 IEnumerable<IGrouping> 转换为 IEnumerable<IEnumerable>?