Linq - Виберіть багато плутанини


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 тепер доступна тут .
Dan Jagnow

Відповіді:


101

Ось ваш запит з використанням SelectMany, змодельований точно за вашим прикладом. Той самий результат!

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

Перший аргумент відображає кожного клієнта до набору замовлень (цілком аналогічних тому, що у вас вже є пункт "де").

Другий аргумент перетворює кожну відповідну пару {(c1, o1), (c1, o2) .. (c3, o9)} у новий тип, який я зробив таким самим, як і ваш приклад.

Так:

  • arg1 відображає кожен елемент базової колекції в іншу колекцію.
  • arg2 (необов’язково) перетворює кожну пару на новий тип

Отримана колекція є плоскою, як ви очікували у вашому оригінальному прикладі.

Якщо б ви опустили другий аргумент, ви отримаєте збір усіх замовлень, які відповідають замовнику. Це було б просто це, плоска колекція Orderпредметів.

Використовуючи це, потрібно багато звикнути, у мене все ще виникають проблеми з обгортанням голови навколо нього. :(


2
Дякуємо за вашу відповідь та пояснення. Це саме те, що мені потрібно було. Дякую також за те, що ви повністю дали відповідь у контексті мого запитання, це набагато легше зрозуміти.
Джекі Кірбі,

1
Заради Піта, чому розміщення .Where () всередині SelectMany () так довго ухилялося від мене ?? Дякую, що вказали на це ...
Тобіас Дж.

Тільки для запису, GroupByможе бути кращим варіантом для цього конкретного сценарію.
Екеву

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>.


3
"(...) і не хвилює елемент, що містить підколекцію." Якщо ви хочете розгладити, і вам все одно, що містить елемент, для цього є перевантаження SelectMany :)
Р. Мартіньо Фернандес

@Keith дякую за вашу відповідь. Як я можу використовувати його для простого збору замовлень?
Джекі Кірбі,

Ваш домен виглядає дещо сумнівним. Замовлення містить Клієнта, який, у свою чергу, містить багато Замовлень?
Бух Бух

@ Buh Buh, жодне Замовлення не містить Клієнта Ідентифікує не Клієнта.
Джекі Кірбі,

1
@ Buh Buh - я це бачив і робив багато разів; це призводить до об’єктного графіку, який можна пройти в будь-якому напрямку, а не лише зверху вниз. Дуже корисно, якщо ваш графік має кілька "точок входу". Якщо ви використовуєте ORM, такий як NHibernate, просто включити зворотне посилання, оскільки воно вже існує в дочірній таблиці. Вам просто потрібно розірвати кругове посилання, заявивши, що каскади йдуть вниз, а не вгору.
KeithS

5

Хоча це давнє запитання, я думав, що трохи покращу чудові відповіді:

SelectMany повертає список (який може бути порожнім) для кожного елемента контрольного списку. Кожен елемент у цих списках результатів перераховується у вихідну послідовність виразів, і тому об'єднується в результат. Отже, список "список -> b" [] -> об'єднати -> список "b".

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}));
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.