Парадигмы C #: побочные эффекты на списках - PullRequest
12 голосов
/ 17 июня 2011

Я пытаюсь развить мое понимание побочных эффектов и того, как их следует контролировать и применять.

В следующем списке рейсов я хочу указать свойство каждого рейса, удовлетворяющее условиям:

IEnumerable<FlightResults> fResults = getResultsFromProvider();

//Set all non-stop flights description
fResults.Where(flight => flight.NonStop)
        .Select(flight => flight.Description = "Fly Direct!");

В этом выражении у меня есть побочный эффект в моем списке. Из моих ограниченных знаний я знаю, например. «LINQ используется только для запросов * только 1007 *» и «Для списков есть только несколько операций, и назначение или установка значений не является одним из них» и «списки должны быть неизменяемыми».

  • Что не так с моим утверждением LINQ выше и как его следует изменить?
  • Где я могу получить больше информации о фундаментальных парадигмах о сценарии, который я описал выше?

Ответы [ 6 ]

13 голосов
/ 17 июня 2011

У вас есть два способа добиться этого, используя способ LINQ:

  1. явный foreach цикл

    foreach(Flight f in fResults.Where(flight => flight.NonStop))
      f.Description = "Fly Direct!";
    
  2. с ForEach оператор, созданный для побочных эффектов:

    fResults.Where(flight => flight.NonStop)
            .ForEach(flight => flight.Description = "Fly Direct!");
    

Первый способ довольно тяжел для такой простой задачи, второй способ следует использовать только с очень короткими телами.

Теперь вы можете спросить себя, почему в стеке LINQ нет оператора ForEach.Это довольно просто - предполагается, что LINQ - это функциональный способ выражения операций запроса, что особенно означает, что ни один из операторов не должен иметь побочных эффектов.Команда разработчиков решила не добавлять оператор ForEach в стек, поскольку единственное использование - это его побочный эффект.

Обычная реализация оператора ForEach будет выглядеть следующим образом:

public static class EnumerableExtension
{
  public static void ForEach<T> (this IEnumerable<T> source, Action<T> action)
  {
    if(source == null)
      throw new ArgumentNullException("source");

    foreach(T obj in source)
      action(obj);

  }
}
6 голосов
/ 17 июня 2011

Одна из проблем этого подхода заключается в том, что он не будет работать вообще. Запрос ленив, что означает, что он не будет выполнять код в Select, пока вы на самом деле не прочитаете что-то из запроса, и вы никогда этого не сделаете.

Вы можете обойти это, добавив .ToList() в конце запроса, но код все еще использует побочные эффекты и выбрасывает фактический результат. Вместо этого вы должны использовать результат для обновления:

//Set all non-stop flights description
foreach (var flight in fResults.Where(flight => flight.NonStop)) {  
  flight.Description = "Fly Direct!";
}
5 голосов
/ 17 июня 2011

Ваш код LINQ не "напрямую" нарушает упомянутые вами рекомендации, потому что вы не изменяете сам список; вы просто изменяете какое-то свойство содержимого списка.

Тем не менее, основное возражение, лежащее в основе этих рекомендаций, остается: вам не следует изменять данные с помощью LINQ (также вы злоупотребляете Select для выполнения побочных эффектов).

Не изменяя любые данные, можно довольно легко обосновать. Рассмотрим этот фрагмент:

fResults.Where(flight => flight.NonStop)  

Вы видите, где это изменяет свойства полета? Как и многие программисты по обслуживанию, они перестанут читать после Where - следующий код явно не содержит побочных эффектов, так как это запрос, верно?

[Nitpick: Конечно, просмотр запроса, возвращаемое значение которого не сохраняется, является мертвой раздачей того, что запрос имеет побочные эффекты или что код должен быть удален; в любом случае, это "что-то не так". Но гораздо проще сказать, что, когда нужно смотреть только две строки кода, а не страницы на страницах.]

Как правильное решение, я бы порекомендовал это:

foreach (var x in fResults.Where(flight => flight.NonStop))
{
    x.Description = "Fly Direct!";
}

Довольно легко писать и читать.

2 голосов
/ 17 июня 2011

Вы должны разбить это на два блока кода, один для извлечения и один для установки значения:

var nonStopFlights = fResults.Where(f => f.NonStop);

foreach(var flight in nonStopFlights)
    flight.Description = "Fly Direct!";

Или, если вы действительно ненавидите внешний вид foreach, вы можете попробовать:

var nonStopFlights = fResults.Where(f => f.NonStop).ToList();

// ForEach is a method on List that is acceptable to make modifications inside.
nonStopFlights.ForEach(f => f.Description = "Fly Direct!");
2 голосов
/ 17 июня 2011

В этом нет ничего плохого, за исключением того, что вам нужно как-то итерировать его, например, вызывая Count().

С точки зрения стиля это нехорошо.Не следует ожидать, что итератор изменяет значение / свойство списка.

IMO будет лучше следующее:

foreach (var x in fResults.Where(flight => flight.NonStop))
{
  x.Description = "Fly Direct!";
}

Намерение намного понятнее читателю или сопровождающему кода.

2 голосов
/ 17 июня 2011

Мне нравится использовать foreach, когда я на самом деле что-то меняю.Что-то вроде

foreach (var flight in fResults.Where(f => f.NonStop))
{
  flight.Description = "Fly Direct!";
}

и Эрик Липперт в своей статье 1006 * о том, почему у LINQ нет вспомогательного метода ForEach.

Но мы можемнемного глубже здесь.Я философски против предоставления такого метода по двум причинам.

Первая причина заключается в том, что это нарушает принципы функционального программирования, на которых основаны все другие операторы последовательности.Очевидно, что единственная цель вызова этого метода - вызвать побочные эффекты.

...