12

我正在努力加深对副作用以及如何控制和应用它们的理解。

在以下航班列表中,我想设置满足条件的每个航班的属性:

IEnumerable<FlightResults> fResults = getResultsFromProvider();

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

在这个表达式中,我的列表有副作用。根据我有限的知识,我知道前任。“LINQ 仅用于查询”和“列表只有少数操作,分配或设置值不是其中之一”和“列表应该是不可变的”。

  • 我上面的 LINQ 语句有什么问题,应该如何更改?
  • 我在哪里可以获得关于我上面描述的场景的基本范式的更多信息?
4

6 回答 6

14

您有两种方法可以通过 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!");
    

对于这样一个简单的任务,第一种方式相当繁重,第二种方式只能用于非常短的身体。

ForEach现在,您可能会问自己为什么LINQ 堆栈中没有运算符。这很简单——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);

  }
}
于 2011-06-17T13:12:58.167 回答
9

这种方法的一个问题是它根本不起作用。查询是惰性的,这意味着它不会执行 Select 中的代码,直到您实际从查询中读取某些内容,并且您永远不会这样做。

您可以通过.ToList()在查询末尾添加来解决这个问题,但代码仍在使用副作用并丢弃实际结果。您应该使用结果来进行更新:

//Set all non-stop flights description
foreach (var flight in fResults.Where(flight => flight.NonStop)) {  
  flight.Description = "Fly Direct!";
}
于 2011-06-17T13:16:21.497 回答
6

您的 LINQ 代码不会“直接”违反您提到的准则,因为您没有修改列表本身;您只是在修改列表内容的某些属性。

但是,推动这些准则的主要反对意见仍然存在:您不应该使用 LINQ 修改数据(而且,您正在滥用Select执行您的副作用)。

不修改任何数据可以很容易地证明是合理的。考虑这个片段:

fResults.Where(flight => flight.NonStop)  

你看到这是在哪里修改航班属性吗?许多维护程序员也不会,因为他们会在之后停止阅读Where——下面的代码 显然没有副作用,因为这是一个查询,对吧?

[Nitpick:当然,看到一个没有保留返回值的查询是一个死的赠品,该查询确实有副作用或者代码应该被删除;无论如何,“有些事情是错误的”。但是,当只有 2 行代码可供查看而不是逐页查看时,说起来要容易得多。]

作为一个正确的解决方案,我会推荐这个:

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

写和读都很容易。

于 2011-06-17T13:18:31.187 回答
2

我喜欢foreach在实际改变某些东西时使用。就像是

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

Eric Lippert 在他关于为什么 LINQ 没有 ForEach 辅助方法的文章中也是如此。

但我们可以在这里更深入一点。我在哲学上反对提供这种方法,原因有两个。

第一个原因是这样做违反了所有其他序列运算符所基于的函数式编程原则。显然,调用此方法的唯一目的是引起副作用。

于 2011-06-17T13:12:44.500 回答
2

它本身并没有什么问题,只是你需要以某种方式迭代它,比如调用Count()它。

从“风格”的角度来看,它并不好。人们不会期望迭代器改变列表值/属性。

IMO以下会更好:

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

对于代码的读者或维护者来说,意图要清楚得多。

于 2011-06-17T13:13:00.567 回答
2

您应该将其分解为两个代码块,一个用于检索,另一个用于设置值:

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!");
于 2011-06-17T13:13:07.240 回答