Linq - SelectMany Confusion

81

Z tego, co rozumiem z dokumentacji SelectMany, można go użyć do stworzenia (spłaszczonej) sekwencji relacji 1-wiele.

Mam następujące zajęcia

  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; }
  }

Następnie próbuję ich użyć, używając składni wyrażenia zapytania w ten sposób

  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);

To daje mi to, czego potrzebuję.

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

Zakładam, że przekłada się to na użycie metody SelectMany, gdy nie używa się składni wyrażenia zapytania?

Tak czy inaczej, próbuję obejść się za pomocą SelectMany. Więc nawet jeśli moje powyższe zapytanie nie jest tłumaczone na SelectMany, biorąc pod uwagę dwie klasy i pozorowane dane, czy ktoś mógłby mi podać zapytanie linq, które używa SelectMany?

Jackie Kirby
źródło
3
Zobacz część 41 serii Edulinq Jona Skeeta . Wyjaśnia proces tłumaczenia wyrażeń zapytań.
R. Martinho Fernandes
2
Myśląc o tym, zobacz także Część 9: SelectMany :)
R. Martinho Fernandes
3
Seria Edulinq Johna Skeeta jest już dostępna tutaj .
Dan Jagnow

Odpowiedzi:

101

Oto twoje zapytanie SelectMany, wzorowane dokładnie na twoim przykładzie. To samo wyjście!

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

Pierwszy argument odwzorowuje każdego klienta na zbiór zamówień (całkowicie analogiczny do już posiadanej klauzuli „gdzie”).

Drugi argument przekształca każdą dopasowaną parę {(c1, o1), (c1, o2) .. (c3, o9)} na nowy typ, który utworzyłem tak samo jak w przykładzie.

Więc:

  • arg1 mapuje każdy element w kolekcji podstawowej do innej kolekcji.
  • arg2 (opcjonalnie) przekształca każdą parę w nowy typ

Powstała kolekcja jest płaska, jak można by się spodziewać po oryginalnym przykładzie.

Jeśli pominąłbyś drugi argument, skończyłbyś z kolekcją wszystkich zamówień pasujących do klienta. To byłaby po prostu płaska kolekcja Orderprzedmiotów.

Używanie go wymaga dużo czasu, aby się do niego przyzwyczaić, ale czasami nadal mam problem z owinięciem głowy wokół niego. :(

Safa
źródło
2
Dziękuję za odpowiedź i wyjaśnienie. Właśnie tego potrzebowałem. Dziękuję również za udzielenie pełnej odpowiedzi w kontekście mojego pytania, dzięki temu jest ono dużo łatwiejsze do zrozumienia.
Jackie Kirby
1
Dla dobra Pete'a, dlaczego umieszczenie .Where () wewnątrz SelectMany () wymykało mi się tak długo? Dzięki za zwrócenie na to uwagi ...
Tobias J,
Tak dla przypomnienia, GroupBymoże być lepszą opcją w tym konkretnym scenariuszu.
Ekevoo
27

SelectMany () działa jak Select, ale z dodatkową funkcją spłaszczania zaznaczonej kolekcji. Powinien być używany zawsze, gdy chcesz rzutować elementy kolekcji podrzędnych i nie przejmuj się tym, który zawiera elementy kolekcji podrzędnej.

Załóżmy na przykład, że Twoja domena wyglądała tak:

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; }
  }

Aby uzyskać tę samą listę, którą chciałeś, Twój Linq wyglądałby mniej więcej tak:

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

... co da ten sam wynik bez konieczności zbierania płaskich zamówień. SelectMany pobiera kolekcję zamówień każdego klienta i iteruje przez nią, aby utworzyć IEnumerable<Order>plik IEnumerable<Customer>.

KeithS
źródło
3
„(...) i nie obchodzi mnie element zawierający kolekcję podrzędną”. Jeśli chcesz spłaszczenia, a zależy ci na elemencie zawierającym, jest za to przeciążenie SelectMany :)
R. Martinho Fernandes
@Keith dzięki za odpowiedź. Jak bym go użył do płaskiej kolekcji zamówień?
Jackie Kirby
Twoja domena wygląda na nieco wątpliwą. Zamówienie zawiera klienta, które z kolei zawiera wiele zamówień?
Buh Buh
@Buh Buh, żadne zamówienie nie zawiera CustomerId, a nie klienta.
Jackie Kirby
1
@Buh Buh - widziałem i robiłem to wiele razy; daje w wyniku wykres obiektu, po którym można przejść w dowolnym kierunku, a nie tylko z góry na dół. Bardzo przydatne, jeśli wykres ma kilka „punktów wejścia”. Jeśli używasz ORM, takiego jak NHibernate, umieszczenie odwołania wstecznego jest trywialne, ponieważ istnieje już w tabeli podrzędnej. Wystarczy przerwać cykliczne odniesienie, stwierdzając, że kaskady opadają, a nie podnoszą się.
KeithS
5

Chociaż jest to stare pytanie, pomyślałem, że poprawię trochę doskonałe odpowiedzi:

SelectMany zwraca listę (która może być pusta) dla każdego elementu listy kontrolnej. Każdy element na tych listach wyników jest wyliczany w sekwencji wyjściowej wyrażeń i dlatego jest łączony z wynikiem. Stąd 'lista -> b' lista [] -> konkatenacja -> b 'lista.

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);
        }
    }
}
George
źródło
1

Oto kolejna opcja przy użyciu SelectMany

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

Jeśli używasz Entity Framework lub LINQ to Sql i masz skojarzenie (relację) między jednostkami, możesz to zrobić:

var customerOrders = customers.SelectMany(
  c => c.orders
   .Select(p => new {CustomerId = c.Id, OrderDescription = p.Description}));
Владимир Береза
źródło