在C#中动态”解压缩”IEnumerable或最佳替代方案

8
假设您有一个返回惰性枚举对象的函数:
struct AnimalCount
{
    int Chickens;
    int Goats;
}

IEnumerable<AnimalCount> FarmsInEachPen()
{
    ....
    yield new AnimalCount(x, y);
    ....
}

此外,您还有两个函数可以消耗两个分离的 IEnumerable,例如:

ConsumeChicken(IEnumerable<int>);
ConsumeGoat(IEnumerable<int>);

如何在不将FarmsInEachPen()转换为列表(因为它可能有数十亿条记录)且没有多线程的情况下调用ConsumeChickenConsumeGoat

基本上:

ConsumeChicken(FarmsInEachPen().Select(x => x.Chickens));
ConsumeGoats(FarmsInEachPen().Select(x => x.Goats));

但是不要强制进行双重枚举。

我可以使用多线程解决它,但对于每个列表,使用一个缓冲区队列会使它变得不必要地复杂。

因此,我正在寻找一种将AnimalCount枚举器分成两个int枚举器的方法,而不必完全评估AnimalCount。同时运行ConsumeGoatConsumeChicken没有问题。

我感觉解决方案就在我的掌握之外,但我还没有到达那里。我考虑了一个帮助函数,返回传入ConsumeChickenIEnumerable,每次使用迭代器时,它都会内部调用ConsumeGoat,从而以锁步方式执行这两个函数。当然,我不想调用ConsumeGoat超过一次..


停下来,深呼吸,把你的想法按照逻辑顺序整理好。你从AnimalCount跳到了IEnumerable<AnimalCounts>再到了IEnumerable<int>(我猜的?甚至无法确定你的意图...)。目前这个问题被标记为不是一个真正的问题。 - Blindy
你为什么要把它们合并成“AnimalCount”呢? - Daniel A. White
@Daniel 假设不是我在将它们组合。我有一个来自外部来源的亿行日志文件,每行都有ChickenCount和GoatCount。我有两个单独的第三方函数,它们希望将它们的数据作为IEnumerable。 - Mahmoud Al-Qudsi
那么我们不能将数组的一半存储在某种临时存储中,对吧? - Blindy
4个回答

4
我认为你想要的事情是无法实现的,因为ConsumeChickens(IEnumerable<int>)ConsumeGoats(IEnumerable<int>)被顺序调用,每个方法都单独枚举一个列表-如果没有两个单独的列表枚举,你如何期望它能正常工作呢?
根据情况不同,更好的解决方案是有ConsumeChicken(int)ConsumeGoat(int)方法(每个方法只消耗一个项目),并交替调用它们。像这样:
foreach(var animal in animals)
{
    ConsomeChicken(animal.Chickens);
    ConsomeGoat(animal.Goats);
}

这将只枚举animals集合一次。
另外,需要注意的是:根据您的LINQ提供程序以及您要执行的确切操作,可能会有更好的选择。例如,如果您想使用linq-to-sql或linq-to-entities从数据库中获取鸡和山羊的总和,则可以使用以下查询。
from a in animals
group a by 0 into g
select new 
{
    TotalChickens = g.Sum(x => x.Chickens), 
    TotalGoats = g.Sum(x => x.Goats)
}

只需一次查询,然后在数据库端计算总和,这比在客户端检索整个表并进行总和计算要好得多。


谢谢你的帮助,但我已经解决了。请看我的回答。 - Mahmoud Al-Qudsi

2
根据您提出的问题,这是不可能的。 `IEnumerable ` 是一种“惰性枚举” - 也就是说,您可以通过使用 `GetEnumerator` 方法查询序列前面的元素,然后重复询问“给我下一个元素”(`MoveNext` / `Current`)。但是在一个线程上,无法同时从 `animals.Select(a => a.Chickens)` 和 `animals.Select(a => a.Goats)` 中检索两个不同的内容。您必须先执行一个然后再执行另一个(这需要实现第二个)。
BlueRaja提出的建议是稍微改变问题的一种方法。我建议您采用这种方式。
另一种选择是利用Microsoft的响应式扩展(Rx)中的 `IObservable ` ,这是一种“推式枚举”。我不会详细介绍如何做到这一点,但这是您可以探讨的内容。
注:以上假设 `ConsumeChickens` 和 `ConsumeGoats` 都返回 `void` 或至少不返回 `IEnumerable ` 本身-这似乎是一个显而易见的假设。如果有人认为文章有问题请及时评论。

2

实现你想要的最简单的方法是将FarmsInEachPen的返回值转换为推送集合或IObservable,并使用ReactiveExtensions来处理它。

var observable = new Subject<Animals>()
observable.Do(x=> DoSomethingWithChicken(x. Chickens))
observable.Do(x=> DoSomethingWithGoat(x.Goats))

foreach(var item in FarmsInEachPen())
{
    observable.OnNext(item)
}   

有趣。这将把 IEnumerable<Chicken,Goat> 投影到 IEnumerable<Chicken>IEnumerable<Goat> 或者我需要修改 DoSomethingWithChicken/Goat 来使用吗? - Mahmoud Al-Qudsi
如果你想以流模式进行操作(即不创建任何其他集合来累积项目),那么你需要编写一些函数来单独接受每个元素,但实际上 Rx 中有将可观察对象转换为可枚举的函数。 - Anton Onikiychuk
后者是我感兴趣的。我将下载 Rx 并看看我能用它做什么,如果它可以做到,我会重新访问这个问题并更改接受的答案。 - Mahmoud Al-Qudsi

1

我弄清楚了,很大程度上要感谢@Lee给我的指引。

你需要在两个zip之间共享一个枚举器,并使用适配器函数将正确的元素投影到序列中。

private static IEnumerable<object> ConsumeChickens(IEnumerable<int> xList)
{
    foreach (var x in xList)
    {
        Console.WriteLine("X: " + x);
        yield return null;
    }
}

private static IEnumerable<object> ConsumeGoats(IEnumerable<int> yList)
{
    foreach (var y in yList)
    {
        Console.WriteLine("Y: " + y);
        yield return null;
    }
}

private static IEnumerable<int> SelectHelper(IEnumerator<AnimalCount> enumerator, int i)
{
    bool c = i != 0 || enumerator.MoveNext();
    while (c)
    {
        if (i == 0)
        {
            yield return enumerator.Current.Chickens;
            c = enumerator.MoveNext();
        }
        else
        {
            yield return enumerator.Current.Goats;
        }
    }
}

private static void Main(string[] args)
{
    var enumerator = GetAnimals().GetEnumerator();

    var chickensList = ConsumeChickens(SelectHelper(enumerator, 0));
    var goatsList = ConsumeGoats(SelectHelper(enumerator, 1));

    var temp = chickensList.Zip(goatsList, (i, i1) => (object) null);
    temp.ToList();

    Console.WriteLine("Total iterations: " + iterations);
}

我无法控制源函数或两个消费者函数。我正在努力使其更美观。 - Mahmoud Al-Qudsi
2
这取决于Enumerable.Zip()交替调用每个序列的枚举器,显然第二个枚举器在第一个之前被调用。文档并不保证这些事实,这意味着这种解决方案可能不总是有效,或者在将来的.Net版本中突然停止工作。 - BlueRaja - Danny Pflughoeft
这是正确的。然而,我确实需要将对象作为列表而不是单个对象(批处理)进行消耗。 - Mahmoud Al-Qudsi
1
如果您无法控制这两个使用者函数,您如何使它们在每次输入后返回 yield return - BlueRaja - Danny Pflughoeft
@BlueRaja 正是我的回答要表达的观点,却遭到了负投票。 - Timothy Shields

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接