Linq - SelectMany Confusion

81

Насколько я понимаю из документации SelectMany, можно было бы использовать его для создания (сглаженной) последовательности отношения 1-многие.

У меня следующие классы

  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, учитывая два класса и фиктивные данные, может ли кто-нибудь предоставить мне запрос linq, который использует SelectMany?

Джеки Кирби
источник
3
См. Часть 41 из серии Edulinq Джона Скита . Он объясняет процесс преобразования выражения запроса.
R. Martinho Fernandes
2
Размышляя об этом, см. Также Часть 9: SelectMany :)
Р. Мартиньо Фернандес
3
Серия Edulinq Джона Скита теперь доступна здесь .
Дэн Ягнов

Ответы:

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
Спасибо за ответ и объяснение. Это именно то, что мне было нужно. Спасибо также за полный ответ в контексте моего вопроса, это делает его намного проще для понимания.
Джеки Кирби
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 берет каждую коллекцию заказов клиентов и перебирает ее, чтобы создать IEnumerable<Order>из файла IEnumerable<Customer>.

KeithS
источник
3
«(...) и не заботиться о элементе, содержащем вложенную коллекцию». Если вам нужно сглаживание и вы заботитесь о содержащем его элементе, для этого есть перегрузка SelectMany :)
Р. Мартинью Фернандес
@Keith спасибо за ваш ответ. Как мне использовать его с плоским набором заказов?
Джеки Кирби
Ваш домен выглядит немного сомнительным. Заказ содержит клиента, который, в свою очередь, содержит много заказов?
Buh Buh
@Buh Buh, никакой Заказ не содержит CustomerId, а не Customer.
Джеки Кирби
1
@Buh Buh - я видел и делал это много раз; в результате получается граф объектов, по которому можно перемещаться в любом направлении, а не только сверху вниз. Очень полезно, если ваш график имеет несколько «точек входа». Если вы используете ORM, например NHibernate, довольно просто включить обратную ссылку, потому что она уже существует в дочерней таблице. Вам просто нужно разорвать круговую ссылку, указав, что каскады идут вниз, а не вверх.
KeithS
5

Хотя это старый вопрос, я подумал, что немного улучшу отличные ответы:

SelectMany возвращает список (который может быть пустым) для каждого элемента контрольного списка. Каждый элемент в этих списках результатов перечисляется в выходной последовательности выражений и, таким образом, объединяется в результат. Следовательно, список '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}));

Если вы используете Entity Framework или LINQ to Sql и у вас есть связь (связь) между сущностями, вы можете сделать это:

var customerOrders = customers.SelectMany(
  c => c.orders
   .Select(p => new {CustomerId = c.Id, OrderDescription = p.Description}));
Владимир Береза
источник