【问题标题】:SelectMany Anonymous Type and Skip IterationsSelectMany 匿名类型和跳过迭代
【发布时间】:2013-11-23 05:31:03
【问题描述】:

当您不总是想返回结果时,我一直在尝试寻找一种“干净”的模式来处理具有匿名类型的 .SelectMany。我最常见的用例如下所示:

  1. 我们有一份我想要报告的客户列表。
  2. 每个客户的数据都驻留在一个单独的数据库中,所以我做了一个并行.SelectMany
  3. 在每个 lambda 表达式中,我为客户收集结果以形成最终报告。
  4. 如果应该跳过特定客户,我需要返回一个空列表。
  5. 我经常使用这些来快速报告,所以我更喜欢匿名类型。

例如,逻辑可能如下所示:

//c is a customer
var context = GetContextForCustomer(c);
// look up some data, myData using the context connection
if (someCondition)
  return myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 });
else
  return null;

这可以作为一个 foreach 语句来实现:

var results = new List<WhatType?>();
foreach (var c in customers) {
  var context = GetContextForCustomer(c);
  if (someCondition)
    results.AddRange(myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 }));
}

或者它可以使用.SelectMany 来实现,该.Where 预先过滤:

customers
  .Where(c => someCondition)
  .AsParallel()
  .SelectMany(c => {
     var context = GetContextForCustomer(c);
     return myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 });
  })
  .ToList();

这两种方法都存在问题。 foreach 解决方案需要初始化一个List 来存储结果,并且您必须定义类型。 .SelectMany.Where 通常是不切实际的,因为someCondition 的逻辑相当复杂并且依赖于一些数据查找。所以我理想的解决方案应该是这样的:

customers
  .AsParallel()
  .SelectMany(c => {
     var context = GetContextForCustomer(c);
     if (someCondition)
       return myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 });
     else
       continue?   return null?   return empty list?
  })
  .ToList();

我应该在else 行中添加什么来跳过返回值?我能想出的解决方案都不是有效的或理想的:

  1. continue 无法编译,因为它不是活动的 foreach 循环
  2. return null 导致 NRE
  3. return空列表需要我重新初始化一个匿名类型的列表。

有没有一种方法可以干净、简单、整洁,并满足我所有(挑剔的)要求?

【问题讨论】:

    标签: c# anonymous-types linq


    【解决方案1】:

    您可以返回一个空的Enumerable&lt;dynamic&gt;。这是一个示例(尽管没有您的客户和someCondition,因为我不知道他们是什么,但与您的示例具有相同的一般形式):

    new int[] { 1, 2, 3, 4 }
        .AsParallel()
        .SelectMany(i => {
            if (i % 2 == 0)
                return Enumerable.Repeat(new { i, squared = i * i }, i);
            else
                return Enumerable.Empty<dynamic>();
            })
        .ToList();
    

    所以,有了你的对象和someCondition,它看起来像

    customers
        .AsParallel()
        .SelectMany(c => {
            var context = GetContextForCustomer(c);
            if (someCondition)
                return myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 });
            else
                return Enumerable.Empty<dynamic>();
           })
        .ToList();
    

    【讨论】:

    • 这会引起大量的动态调用,进而引起反射。就性能而言,这不是最佳选择。
    • 好点。但他也说他只需要这个来生成一些快速报告。
    • 我最喜欢这个解决方案,因为它很短并且不需要任何自定义扩展方法。我试过Enumerable.Empty(),但它无法确定类型,没有&lt;dynamic&gt;。谢谢!!
    • 如果你这样做,最终的返回类型也将是IEnumerable&lt;dynamic&gt;。根据您的用例,这可能会正常工作。
    【解决方案2】:

    不知道someConditionmyData 长什么样……

    你为什么不把上下文也用SelectWhere

    customers
    .Select(c => GetContextForCustomer(c))
    .Where(ctx => someCondition)
    .SelectMany(ctx => 
        myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 });
    

    编辑:我刚刚意识到您需要进一步携带 customercontext,所以您可以这样做:

    customers
    .Select(c => new { Customer = c, Context = GetContextForCustomer(c) })
    .Where(x => someCondition(x.Context))
    .SelectMany(x => 
        myData.Select(d => new { CustomerID = x.Customer, X1 = d.x1, X2 = d.x2 });
    

    【讨论】:

    • 我意识到我的例子并不公平。实际上,someConditionmyData 背后的逻辑是 20-30 行代码并且发展迅速,所以我最喜欢将它们放在 .SelectMany 中。
    【解决方案3】:

    您可以尝试以下操作:

    customers
      .AsParallel()
      .SelectMany(c => {
         var context = GetContextForCustomer(c);
         if (someCondition)
           return myData.Select(x => new { CustomerID = c, X1 = x.x1, X2 = x.x2 });
         else
           return Enumerable.Empty<int>().Select(x => new { CustomerID = 0, X1 = "defValue", X2 = "defValue" });
      })
      .ToList();
    

    所有具有相同属性集(相同名称和类型)的匿名类型被编译器组合成一个匿名类。这就是为什么你的SelectEnumerable.Empty 上的T 都会返回相同的T

    【讨论】:

    • 这个解决方案基本上与foreachreturn 空列表在同一条船上——你必须动态地构造一个匿名类型的列表。例如,我知道像 this one 这样的解决方案。它有效,但现在我有两个属性列表,如果我决定在动态报告中添加/删除属性,我必须更新。
    【解决方案4】:

    您可以创建自己的SelectMany LINQ 方法的变体,它支持nulls:

    public static class EnumerableExtensions
    {
        public static IEnumerable<TResult> NullableSelectMany<TSource, TResult> (
            this IEnumerable<TSource> source,
            Func<TSource, IEnumerable<TResult>> selector)
        {
            if (source == null) 
                throw new ArgumentNullException("source");
            if (selector == null) 
                throw new ArgumentNullException("selector");
            foreach (TSource item in source) {
                IEnumerable<TResult> results = selector(item);
                if (results != null) {
                    foreach (TResult result in results)
                        yield return result;
                }
            }
        }
    }
    

    现在您可以在 selector lambda 中返回 null

    【讨论】:

    • 这是一个不错的方法。我考虑的另一件事是.Select(...).Where(x =&gt; x != null).SelectMany(x =&gt; x)。不那么漂亮,但不需要我在任何地方指定匿名类型。
    • @mellamokb ReSharper 实际上向我建议了这一点。 :) 我也有很多IEnumerable 的扩展方法,所以我会自己写.Select(...).WhereNotNull().Flatten() 之类的东西,这对我来说已经足够漂亮了。
    【解决方案5】:

    接受的答案返回dynamic。最干净的方法是将过滤逻辑移动到 Where 中,这使得整个事情在 linq 上下文中看起来更好。由于您在问题中明确排除了这一点,而且我不喜欢在 linq 调用中多行编写代表,我将尝试this,但有人可能会认为它更hacky。

    var results = new 
    { 
        customerID = default(int), //notice the casing of property names
        x1 = default(U), //whatever types they are
        x2 = default(V) 
    }.GetEmptyListOfThisType();
    
    foreach (var customerID in customers) {
      var context = GetContextForCustomer(customerID);
      if (someCondition)
        results.AddRange(myData.Select(x => new { customerID, x.x1, x.x2 }));
    }
    
    public static List<T> GetEmptyListOfThisType<T>(this T item)
    {
        return new List<T>();
    }
    

    注意属性名称的正确使用与其他变量名称一致,因此您不必在Select 调用中再次编写属性名称。

    【讨论】:

      猜你喜欢
      • 2023-02-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-05
      • 2011-09-03
      • 2011-08-06
      相关资源
      最近更新 更多