Замена вложенного foreach на LINQ; изменить и обновить свойство глубоко внутри - PullRequest
13 голосов
/ 26 августа 2009

Рассмотрим требование изменить элемент данных в одном или нескольких свойствах объекта глубиной 5 или 6 уровней.

Существуют вложенные коллекции, которые необходимо перебрать, чтобы получить свойство, которое требует проверки и модификации.

Здесь мы вызываем метод, который очищает уличный адрес сотрудника. Поскольку мы меняем данные внутри циклов, текущей реализации необходим цикл for для предотвращения исключения:

Невозможно присвоить «someVariable», поскольку это «переменная итерации foreach»

Вот текущий алгоритм (обфусцированный) с вложенным foreach и for.

foreach (var emp in company.internalData.Emps)
{
    foreach (var addr in emp.privateData.Addresses)
    {
        int numberAddresses = addr.Items.Length;

        for (int i = 0; i < numberAddresses; i++)
        {
            //transform this street address via a static method
            if (addr.Items[i].Type =="StreetAddress")
               addr.Items[i].Text = CleanStreetAddressLine(addr.Items[i].Text);
        }
    }
}

Вопрос: Можно ли переопределить этот алгоритм с помощью LINQ? Требуется, чтобы исходная коллекция изменила данные при вызове этого статического метода.

Обновление: Я думал / склонялся в направлении решения типа jQuery / селектор. Я специально не сформулировал этот вопрос таким образом. Я понимаю, что я переборщил с этой идеей (без побочных эффектов). Спасибо всем! Если есть такой способ выполнения jQuery-подобного селектора, давайте посмотрим на это!

Ответы [ 8 ]

19 голосов
/ 26 августа 2009
foreach(var item in company.internalData.Emps
                        .SelectMany(emp => emp.privateData.Addresses)
                        .SelectMany(addr => addr.Items)
                        .Where(addr => addr.Type == "StreetAddress"))
     item.Text = CleanStreetAddressLine(item.Text);
13 голосов
/ 26 августа 2009
var dirtyAddresses = company.internalData.Emps.SelectMany( x => x.privateData.Addresses )
                                              .SelectMany(y => y.Items)
                                              .Where( z => z.Type == "StreetAddress");

  foreach(var addr in dirtyAddresses)
    addr.Text = CleanStreetAddressLine(addr.Text);
12 голосов
/ 26 августа 2009

LINQ не предназначен для изменения наборов объектов. Вы не ожидаете, что оператор SELECT sql изменит значения выбранных строк, не так ли? Это помогает запомнить, что означает LINQ - L anguage IN tegrated Q uery. Изменение объектов в запросе linq, IMHO, является анти-шаблоном.

Ответ Стэна Р. был бы лучшим решением с использованием цикла foreach , я думаю.

10 голосов
/ 26 августа 2009

Мне не нравится смешивать синтаксис "понимание запросов" и синтаксис вызова точечных методов в одном выражении.

Мне нравится идея отделить запрос от действия . Они семантически различны, поэтому разделение их в коде часто имеет смысл.

var addrItemQuery = from emp in company.internalData.Emps
                    from addr in emp.privateData.Addresses
                    from addrItem in addr.Items
                    where addrItem.Type == "StreetAddress"
                    select addrItem;

foreach (var addrItem in addrItemQuery)
{
    addrItem.Text = CleanStreetAddressLine(addrItem.Text);
}

Несколько заметок о стиле вашего кода; это личные, поэтому я не согласен:

  • В общем, я избегаю сокращений (Emps, emp, addr)
  • Несоответствующие имена более запутанные (addr против Addresses): выберите одно и придерживайтесь его
  • Слово «число» неоднозначно. Это может быть либо личность («Заключенный № 378, пожалуйста, шаг вперед».), Либо счет («количество овец в этом поле - 12»). Поскольку мы часто используем обе концепции в коде, очень важно прояснить это. Я часто использую «index» для первого и «count» для второго.
  • Наличие поля type в виде строки является запахом кода. Если вы можете сделать это enum, ваш код, вероятно, будет лучше.
2 голосов
/ 26 августа 2009

Грязный однострочный.

company.internalData.Emps.SelectMany(x => x.privateData.Addresses)
    .SelectMany(x => x.Items)
    .Where(x => x.Type == "StreetAddress")
    .Select(x => { x.Text = CleanStreetAddressLine(x.Text); return x; });
1 голос
/ 26 августа 2009

Вы можете сделать это, но вы действительно не хотите. Несколько блогеров говорили о функциональной природе Linq, и если вы посмотрите на все предоставленные MS методы Linq, вы обнаружите, что они не вызывают побочных эффектов. Они выдают возвращаемые значения, но больше ничего не меняют. Ищите аргументы по методу Linq ForEach, и вы получите хорошее объяснение этой концепции.

Имея это в виду, что вы, вероятно, хотите, что-то вроде этого:

var addressItems = company.internalData.Emps.SelectMany(
    emp => emp.privateData.Addresses.SelectMany(
           addr => addr.Items
    )
);
foreach (var item in addressItems)
{
   ...
}

Однако, если вы хотите сделать именно то, что вы просили, то вам нужно идти в этом направлении:

var addressItems = company.internalData.Emps.SelectMany(
    emp => emp.privateData.Addresses.SelectMany(
           addr => addr.Items.Select(item =>
           { 
              // Do the stuff
              return item;
           }) 
    )
);
1 голос
/ 26 августа 2009

LINQ не предоставляет возможность побочных эффектов. однако вы можете сделать:

company.internalData.Emps.SelectMany(emp => emp.Addresses).SelectMany(addr => Addr.Items).ToList().ForEach(/*either make an anonymous method or refactor your side effect code out to a method on its own*/);
0 голосов
/ 19 апреля 2012

Чтобы обновить результат LINQ с помощью цикла FOREACH, я сначала создаю локальную переменную list , а затем выполняю обновление с помощью цикла FOREACH. Значение обновляется таким образом. Подробнее здесь:

Как обновить значение результатов LINQ, используя цикл FOREACH

...