这种C#扩展方法是不纯的,如果是这样,代码不好?

时间:2009-03-28 22:13:45

标签: c# functional-programming extension-methods

我正在学习函数编程,我想知道:

1)如果我的ForEach扩展方法是纯粹的?我打电话的方式似乎违反了“不要把对象弄得乱七八糟”,对吧?

public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
  foreach ( var item in source )
     action(item);
}


static void Main(string[] args)
{
    List<Cat> cats = new List<Cat>()
    {
        new Cat{ Purring=true,Name="Marcus",Age=10},
        new Cat{ Purring=false, Name="Fuzzbucket",Age=25 },
        new Cat{ Purring=false, Name="Beanhead",Age=9 },
        new Cat{Purring=true,Name="Doofus",Age=3}
    };


    cats.Where(x=>x.Purring==true).ForEach(x =>
    {
        Console.WriteLine("{0} is a purring cat... purr!", x.Name);
    });

    // *************************************************
    //  Does this code make the extension method impure?
    // *************************************************
    cats.Where(x => x.Purring == false).ForEach(x =>
    {
        x.Purring = true; // purr,baby
    });

    // all the cats now purr
    cats.Where(x=>x.Purring==true).ForEach(x =>
    {
        Console.WriteLine("{0} is a purring cat... purr!", x.Name);
    });
}

public class Cat {
        public bool Purring;
        public string Name;
        public int Age;
}

2)如果不纯,那么代码是不是很糟糕?我个人认为它使代码比旧的foreach ( var item in items) { blah; }更清晰,但我担心,因为它可能是不纯的,它可能会弄得一团糟。

3)如果它返回IEnumerable<T>而不是void,会不会是错误的代码?我会说,只要它是不纯的,是的,这将是非常糟糕的代码,因为它会鼓励链接一些会修改链的东西。例如,这是不好的代码吗?

// possibly bad extension
public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
    foreach ( var item in source )
        action(item);

    return source;

}

4 个答案:

答案 0 :(得分:12)

杂质并不一定意味着糟糕的代码。许多人发现使用副作用解决问题很容易和有用。关键是首先要知道如何以纯粹的方式进行,所以你应该知道什么时候杂质是合适的:)。

.NET在类型系统中没有纯度的概念,因此接受任意委托的“纯”方法总是不纯的,这取决于它的调用方式。例如,“Where”,又名“filter”,通常被认为是纯函数,因为它不会修改其参数或修改全局状态。

但是,没有什么可以阻止你将这些代码放在Where的参数中。例如:

things.Where(x => { Console.WriteLine("um?"); 
                    return true; })
      .Count();

所以这绝对是Where的不纯用法。 Enumerables可以在迭代时做任何他们想做的事。

您的代码是否错误?否。使用foreach循环就像“不纯”一样 - 您仍然在修改源对象。我一直在编写这样的代码。将一些选择,过滤器等链接在一起,然后在其上执行ForEach以调用某些工作。你是对的,它更干净,更容易。

示例:ObservableCollection。由于某种原因,它没有AddRange方法。所以,如果我想添加一些东西,我该怎么办?

foreach(var x in things.Where(y => y.Foo > 0)) { collection.Add(x)); } 

things.Where(x => x.Foo > 0).ForEach(collection.Add);

我更喜欢第二个。至少,我看不出它如何被解释为比第一种方式更糟糕。

什么时候代码不好?当它在一个不期望的地方执行副作用代码时。这是我使用Where的第一个例子的情况。即便如此,有时候范围非常有限且使用情况也很明确。

Chaining ForEach

我编写的代码就是这样做的。为了避免混淆,我会给它另一个名字。主要的困惑是“这是立即评估还是懒惰?”。 ForEach意味着它将立即执行一个循环。但是返回IEnumerable的东西意味着将根据需要处理这些项目。所以我建议给它另一个名字(“Process”,“ModifySeq”,“OnEach”......类似的东西),并让它变得懒惰:

public static IEnumerable<T> OnEach(this IEnumerable<T> src, Action<T> f) {
  foreach(var x in src) {
    f(x);
    yield return x;
  }
}

答案 1 :(得分:6)

它不纯,因为它可以称为不纯的方法。我认为通过典型的定义,纯度是一个传递闭包 - 只有当它调用的所有函数(直接或间接)都是纯粹的,或者如果这些函数的效果被封装时(例如它们只是改变非转义),函数才是纯的。局部变量)。

答案 2 :(得分:4)

是的,它不是纯粹的,但这是一个没有实际意义的点,因为它甚至不是一个功能。

由于该方法不返回任何内容,因此它执行任何操作的唯一选择是要么影响要发送的对象,要么影响不相关的内容(例如写入控制台窗口)。

编辑:
回答你的第三个问题;是的,这是糟糕的代码,因为它似乎正在做一些它不做的事情。该方法返回一个集合,因此它似乎是纯粹的,但由于它只返回发送的集合,它实际上不比第一个版本更纯。要理解该方法应该使用Func<T,T>委托作为转换,并返回转换项的集合:

public static IEnumerable<T> ForEach<T>(this IEnumerable<T> source, Func<T,T> converter) {
   foreach (T item in source) {
      yield return converter(item);
   }
}

如果分机呼叫是纯粹的,那么当然还有转换器功能。如果它没有复制输入项但只是更改它并返回它,那么调用仍然不是纯粹的。

答案 3 :(得分:3)

的确,因为你的lambda表达式包含一个赋值,所以函数现在定义为不纯。赋值是否与其中一个参数或在当前函数之外定义的另一个对象相关是无关紧要的...函数必须没有任何副作用才能被称为 pure 。有关更精确(但非常简单)的定义,请参阅Wikipedia,其中详细说明了函数必须满足的两个条件才能被视为 pure (没有副作用就是其中之一)。我相信lambda表达式通常意味着用作纯函数(至少我认为它们最初是从数学角度研究的),尽管C#对于纯粹的函数式语言来说并不严格。所以这可能不是很糟糕,尽管这样的功能是不纯的,但绝对值得离开。