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.