将延迟的IEnumerable<T>分成两个序列而不重新评估?

9
我有一个方法需要处理一系列指令,并根据结果的某些属性将结果分成不同的桶中。例如:
class Pets
{
    public IEnumerable<Cat> Cats { get; set; }
    public IEnumerable<Dog> Dogs { get; set; }
}

Pets GetPets(IEnumerable<PetRequest> requests) { ... }

底层模型完全能够一次性处理整个 PetRequest 元素序列,而且 PetRequest 大多是像 ID 这样的通用信息,因此在输入时尝试拆分请求是没有意义的。但是提供程序实际上并不返回 CatDog 实例,而只返回一个通用数据结构。
class PetProvider
{
    IEnumerable<PetData> GetPets(IEnumerable<PetRequest> requests)
    {
        return HandleAllRequests(requests);
    }
}

我已将响应类型的名称更改为PetData,而不是Pet,以清楚地指示它不是CatDog的超类 - 换句话说,转换为CatDog是一个映射过程。 另一件需要记住的事情是,HandleAllRequests很昂贵,例如数据库查询,所以我真的不想重复它,并且我更喜欢避免使用ToArray()或类似方法在内存中缓存结果,因为可能会有数以千计甚至数百万的结果(我有很多宠物)。到目前为止,我已经能够拼凑出这个笨拙的hack:
Pets GetPets(IEnumerable<PetRequest> requests)
{
    var data = petProvider.GetPets(requests);
    var dataGroups = 
        from d in data
        group d by d.Sound into g
        select new { Sound = g.Key, PetData = g };
    IEnumerable<Cat> cats = null;
    IEnumerable<Dog> dogs = null;
    foreach (var g in dataGroups)
        if (g.Sound == "Bark")
            dogs = g.PetData.Select(d => ConvertDog(d));
        else if (g.Sound == "Meow")
            cats = g.PetData.Select(d => ConvertCat(d));
    return new Pets { Cats = cats, Dogs = dogs };
}

从技术上讲,这种方法是可行的,因为它不会导致PetData结果被枚举两次,但它有两个主要问题:

  1. 它在代码中看起来像一个巨大的痘子;它充满了我们在LINQ框架2.0之前必须使用的可怕的命令式风格。

  2. 它最终成为一次完全无意义的尝试,因为GroupBy方法只是将所有这些结果缓存到内存中,这意味着我并没有比如果我一开始就懒惰地做了一个ToList()并附加了一些谓词更好。

所以重新阐述问题:

是否可能将单个延迟的IEnumerable<T>实例分成两个IEnumerable<?>实例,而不进行任何急切的评估,在内存中缓存结果,或者不得不重新评估原始的IEnumerable<T>第二次?

基本上,这将是一个Concat操作的反向过程。 .NET框架中还没有这样的操作,这表明这可能根本不可能,但我认为问一下也无妨。

P.S.请不要告诉我创建一个Pet超类并只返回IEnumerable<Pet>。我使用CatDog作为有趣的例子,但实际上结果类型更像ItemError - 它们都源自相同的通用数据,但在其他方面完全没有共同之处。


听起来你想把一个延迟序列分成两个延迟序列,是这样吗? - Gabe
@Gabe:是的,输出序列也必须被延迟(否则它们将被缓存/急切加载)。 - Aaronaught
能否分别查询猫和狗的数据源?你说可能会有成千上万的结果 - 这在我的脑海中引起了警觉。 - Christian Hayter
2个回答

12

从根本上来说,不行。想象一下如果这是可能的。然后考虑一下如果我这样做会发生什么:

foreach (Cat cat in pets.Cats)
{
    ...
}

foreach (Dog dog in pets.Dogs)
{
    ...
}

首先需要处理所有猫,然后处理所有狗...那么如果第一个元素是Dog,原始顺序会发生什么情况?它要么需要缓存它,要么跳过它 - 它不能返回它,因为我们仍在请求Cats

你可以实现一些只缓存所需内容的东西,但通常使用方式是完全评估其中一个序列或另一个序列。

如果可能,最好只在获取宠物(无论是猫还是狗)时处理它们。提供一个Action<Cat>和一个Action<Pet>,并对每个项目执行正确的处理程序,这样做是否可行?


最后一段中关于Action<T>参数的部分很有趣。我试图在问题中省略琐碎的细节,但实际上Pets类的现实等价物最终注定要进行序列化。理论上,我想它可能可以这样做(即直接将数据写入文件/套接字)-不幸的是,这可能涉及到比我有时间重新架构更多的工作。 - Aaronaught
@Aaronaught:Func<Cat, byte[]>怎么样?换句话说,“给我这只猫的序列化形式”?基本上,您想要挂钩以便在获取所有值时可以处理它们。 - Jon Skeet
是的,没错...但这里有一些细节需要注意,如果将实体序列化为无序状态,那么会破坏模式,因为它期望所有的“猫”之后才是所有的“狗”(就这么说吧)。不过,对于一般情况来说,这仍然是一个好建议。 - Aaronaught
5
如果所有的狗都必须在所有的猫之前到达,那么你就没有选择余地了——一定要有某些缓存。现在,这可能是序列化形式——将狗序列化到“真实”流中,将猫序列化到MemoryStream中,然后在到达宠物末尾时将MemoryStream复制到真实流中。从根本上考虑,如果你首先读取了一只猫,你可以做什么——处理它、忘记它或者缓存它。没有其他选择 :) - Jon Skeet
2
经过进一步思考,我认为“根据需要缓存”策略在这种情况下实际上可能起作用。在大多数情况下,大多数结果将在第一个桶(Cat)中,因此我可以尝试编写一个系统,将“特殊”结果(Dog)和迭代“正常”结果的结果刷新。 - Aaronaught
@Aaronaught:对于“finagle”,点赞+1。 - Andras Zoltan

2

我想你可能需要将一个deferred IEnumerable拆分成两个序列,而不进行重新评估。以下是Jon的建议

我会选择老派的方法来处理:

List<Cat> cats = new List<Cat>();
List<Dog> dog = new List<Dog>();

foreach(var pet in data)
{
   if (g.Sound == "Bark")
     dogs.Add(ConvertDog(pet));
   else if (pet.Sound == "Meow")
     cats.Add(ConvertCat(pet));
}

但是我意识到这不完全是您想要做的 - 但是您说过 重新评估 - 而这只会评估一次 :)


这实际上正是我不想做的事情:将数据缓存在内存中。由于变换是乘法(大小方面),急切地加载整个输入序列并基于它返回两个“Where”可枚举对象会更有效率。 - Aaronaught
@Aaronaught - Rx 有帮助吗?http://msdn.microsoft.com/en-us/data/gg577609 - 然后,您可以通过枚举器立即提取对象,而无需使用 List<T>,并在完成时转储它们。 - Andras Zoltan
1
这可能是我正在考虑的选项之一,但到目前为止,我还没有能够想出任何接近具体解决方案的东西,只有一个类似模糊的想法。 - Aaronaught
@Aaronaught,是的,我必须承认,尽管它显然很酷,但我还没有找到理由来证明使用甚至是正确地尝试 Rx 的必要性,因此我无法建议如何使用它!同样,我认为尝试将其与您需要对结果进行排序的需求结合使用将提供一个“有趣”的挑战 :) - Andras Zoltan

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