Linq-SelectMany混乱


81

据我从SelectMany的文档中了解到的,可以用它来产生一个(平整的)一对多关系的序列。

我有以下课程

  public class Customer
  {
    public int Id { get; set; }
    public string Name { get; set; }
  }

  class Order
  {
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public string Description { get; set; }
  }

然后,我尝试使用查询表达式语法使用它们,如下所示

  var customers = new Customer[]
  {
    new Customer() { Id=1, Name ="A"},
    new Customer() { Id=2, Name ="B"},
    new Customer() { Id=3, Name ="C"}
  };

  var orders = new Order[]
  {
    new Order { Id=1, CustomerId=1, Description="Order 1"},
    new Order { Id=2, CustomerId=1, Description="Order 2"},
    new Order { Id=3, CustomerId=1, Description="Order 3"},
    new Order { Id=4, CustomerId=1, Description="Order 4"},
    new Order { Id=5, CustomerId=2, Description="Order 5"},
    new Order { Id=6, CustomerId=2, Description="Order 6"},
    new Order { Id=7, CustomerId=3, Description="Order 7"},
    new Order { Id=8, CustomerId=3, Description="Order 8"},
    new Order { Id=9, CustomerId=3, Description="Order 9"}
  };

  var customerOrders = from c in customers
                       from o in orders
                       where o.CustomerId == c.Id
                       select new 
                              { 
                                 CustomerId = c.Id
                                 , OrderDescription = o.Description 
                              };

  foreach (var item in customerOrders)
    Console.WriteLine(item.CustomerId + ": " + item.OrderDescription);

这满足了我的需求。

1: Order 1
1: Order 2
1: Order 3
1: Order 4
2: Order 5
2: Order 6
3: Order 7
3: Order 8
3: Order 9

我假设这在不使用查询表达式语法时转换为使用SelectMany方法吗?

无论哪种方式,我都在尝试使用SelectMany来解决问题。因此,即使我的上述查询没有转换为SelectMany,在给定两个类和模拟数据的情况下,有人可以为我提供使用SelectMany的linq查询吗?


3
请参阅Jon Skeet的Edulinq系列的第41部分。它说明了查询表达式转换过程。
R. Martinho Fernandes

2
关于它的思考,另请参阅第9部分:SelectMany :)
R. Martinho Fernandes

3
John Skeet的Edulinq系列现在可以在这里获得
Dan Jagnow 2014年

Answers:


101

这是您使用进行查询的查询SelectMany,完全按照您的示例进行建模。输出相同!

        var customerOrders2 = customers.SelectMany(
            c => orders.Where(o => o.CustomerId == c.Id),
            (c, o) => new { CustomerId = c.Id, OrderDescription = o.Description });

第一个参数将每个客户映射到一组订单(完全类似于您已有的“ where”子句)。

第二个参数将每个匹配对{(c1,o1),(c1,o2)..(c3,o9)}转换为新类型,我与您的示例相同。

所以:

  • arg1将基础集合中的每个元素映射到另一个集合。
  • arg2(可选)将每对转换为新类型

结果集合像您在原始示例中所期望的那样是平坦的。

如果您忽略第二个参数,那么最终将得到与客户匹配的所有订单的集合。仅仅是Order对象的平面集合。

使用它需要花费很多时间来适应,有时我仍然难以解决。:(


2
感谢您的回答和解释。那正是我所需要的。也感谢您在我的问题范围内完全提供了答案,这使它更容易理解。
Jackie Kirby

1
看在皮特的份上,为什么把.Where()放到SelectMany()里面这么久呢?感谢您指出这一点……
Tobias J

仅作记录,GroupBy对于这种特定情况可能是更好的选择。
Ekevoo

27

SelectMany()的工作方式与Select相似,但具有使所选集合变平的额外功能。每当您希望对子集合的元素进行投影并且不关心子集合的包含元素时,都应使用它。

例如,假设您的域如下所示:

public class Customer
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Order> Orders { get; set; }
  }

  class Order
  {
    public int Id { get; set; }
    public Customer Customer { get; set; }
    public string Description { get; set; }
  }

为了获得所需的相同列表,Linq看起来像这样:

var customerOrders = Customers
                        .SelectMany(c=>c.Orders)
                        .Select(o=> new { CustomerId = o.Customer.Id, 
                                           OrderDescription = o.Description });

...这将产生相同的结果,而无需统一收集订单。SelectMany接受每个客户的Orders集合,并对其进行迭代以从中产生IEnumerable<Order>一个IEnumerable<Customer>


3
“(...)不必关心子集合的包含元素。” 如果您想展平,并且确实在意包含元素,那么SelectMany会有很多过载:)
R. Martinho Fernandes

@Keith谢谢您的回答。如何将其与固定的订单集合一起使用?
Jackie Kirby

您的域看起来有点可疑。订单包含一个客户,而客户又包含许多订单?
Buh Buh

@Buh Buh,没有订单包含CustomerId而不是Customer。
Jackie Kirby

1
@Buh Buh-我已经看过很多次了;它导致对象图可以沿任何方向遍历,而不仅仅是自上而下。如果图形具有多个“入口点”,则非常有用。如果您使用像NHibernate这样的ORM,则包含反向引用很简单,因为它已经存在于子表中。您只需要声明级联下降而不是上升就可以打破循环引用。
KeithS 2011年

5

尽管这是一个古老的问题,但我认为我会稍微改善一些出色的答案:

SelectMany为控制列表的每个元素返回一个列表(可能为空)。这些结果列表中的每个元素都被枚举到表达式的输出序列中,因此被串联到结果中。因此,a'list-> b'list []-> concatenate-> b'list。

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using System.Diagnostics;
namespace Nop.Plugin.Misc.WebServices.Test
{
    [TestClass]
    public class TestBase
    {
        [TestMethod]
        public void TestMethod1()
        {  //See result in TestExplorer - test output 
            var a = new int[]{7,8};
            var b = new int[]
                    {12,23,343,6464,232,75676,213,1232,544,86,97867,43};
            Func<int, int, bool> numberHasDigit = 
                    (number
                     , digit) => 
                         ( number.ToString().Contains(digit.ToString()) );

            Debug.WriteLine("Unfiltered: All elements of 'b' for each element of 'a'");
            foreach(var l in a.SelectMany(aa => b))
                Debug.WriteLine(l);
            Debug.WriteLine(string.Empty);
            Debug.WriteLine("Filtered:" +  
            "All elements of 'b' for each element of 'a' filtered by the 'a' element");
            foreach(var l in a.SelectMany(aa => b.Where(bb => numberHasDigit(bb, aa))))
                Debug.WriteLine(l);
        }
    }
}

1

这是使用SelectMany的另一个选项

var customerOrders = customers.SelectMany(
  c => orders.Where(o => o.CustomerId == c.Id)
    .Select(p => new {CustomerId = c.Id, OrderDescription = p.Description}));

如果您使用实体框架或LINQ to Sql,并且实体之间具有关联(关系),则可以这样做:

var customerOrders = customers.SelectMany(
  c => c.orders
   .Select(p => new {CustomerId = c.Id, OrderDescription = p.Description}));
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.