【问题标题】:C# Paradigms: Side effects on ListsC# 范式:列表的副作用
【发布时间】:2011-09-17 04:32:26
【问题描述】:

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

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

IEnumerable<FlightResults> fResults = getResultsFromProvider();

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

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

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

【问题讨论】:

  • 使用“foreach”循环来引发副作用。选择是为了投影,而不是为了更新。这就是为什么它被称为“选择”而不是“更新”。

标签: c# linq list paradigms side-effects


【解决方案1】:

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

  }
}

【讨论】:

  • 哦,我不知道。有趣。
  • @leppie 实际上查询不会按预期工作。在 OP 代码块的末尾,值不会更改。但是,是的,OP 中的语句将编译/运行。
  • @Femaref:就地分配有时很方便。例如(对于具有惰性初始化程序的属性)return x ?? (x = new Foo());。但我不会在调用Select 时使用它:)
  • @Rangoric: 应该添加一个需要迭代的迭代,例如调用Count()
  • 列表(或 LINQ 意义上的:序列)不会有副作用,只有运算符可以。正如ForEach 运算符明确指出的那样,它具有副作用,是的,它是可以接受的。在您的情况下,Select 不仅投射到某些东西(项目意味着接受输入,以仅考虑原始输入的方式对其进行转换),它还会改变它,这与功能范式背道而驰。跨度>
【解决方案2】:

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

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

//Set all non-stop flights description
foreach (var flight in fResults.Where(flight => flight.NonStop)) {  
  flight.Description = "Fly Direct!";
}

【讨论】:

  • 是的,我确实意识到了这一点。但是,我可以在示例中使用 .Select(...) 并在我想显示结果之前使用 .ToList() 吗?
  • @Pierre:如果您只想显示结果,则根本不需要ToList。您问题中代码的结果显示起来毫无用处,因为它只是一个相同的字符串列表。
【解决方案3】:

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

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

不修改任何数据可以很容易地证明是合理的。考虑一下这个 sn-p:

fResults.Where(flight => flight.NonStop)  

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

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

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

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

写和读都很容易。

【讨论】:

    【解决方案4】:

    它本身并没有什么问题,只是您需要以某种方式对其进行迭代,例如在其上调用Count()

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

    IMO 以下会更好:

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

    代码的读者或维护者更清楚其意图。

    【讨论】:

      【解决方案5】:

      你应该把它分成两段代码,一段用于检索,一段用于设置值:

      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!");
      

      【讨论】:

      • +1 我正要发布一个关于整个“从不真正改变它需要的东西”的答案。
      【解决方案6】:

      我喜欢在实际更改某些内容时使用foreach。类似的东西

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

      Eric Lippert 在他的 article 中也谈到了为什么 LINQ 没有 ForEach 辅助方法。

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

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

      【讨论】:

      • 好文章,解释了我的问题的一些核心想法,谢谢。
      最近更新 更多