C# 迭代器中的副作用被认为是有害的?

C# 迭代器中的副作用被认为是有害的?,c#,oop,iterator,C#,Oop,Iterator,今天我写了我的第一个C#迭代器。呜呼 有趣的是,它有副作用。我的迭代器从目录中过滤出无效文件,并返回要处理的有效文件序列。当它遇到invlay文件时,它会将其移动到另一个目录 我尝试将其实现为LINQ查询,但确实不喜欢where子句的谓词有副作用这一事实。那是一种确定的气味 我可以显式地实现它,在所有文件上循环,依次处理好的或坏的,但它不是很优雅。更好的解决方案是将其分为两个列表(好列表和坏列表),并依次处理每个列表 但后来我想起了迭代器。现在我有了一个迭代器,它生成有效的文件并处理(移动)无效

今天我写了我的第一个C#迭代器。呜呼

有趣的是,它有副作用。我的迭代器从目录中过滤出无效文件,并返回要处理的有效文件序列。当它遇到invlay文件时,它会将其移动到另一个目录

我尝试将其实现为LINQ查询,但确实不喜欢where子句的谓词有副作用这一事实。那是一种确定的气味

我可以显式地实现它,在所有文件上循环,依次处理好的或坏的,但它不是很优雅。更好的解决方案是将其分为两个列表(好列表和坏列表),并依次处理每个列表

但后来我想起了迭代器。现在我有了一个迭代器,它生成有效的文件并处理(移动)无效的文件

所以,我的问题是:迭代器产生这样的副作用是个坏主意吗?我是不是在迭代器中隐藏了太多的功能?

我想说,在迭代器中产生副作用通常是个坏主意,但这并不是完全禁止的。如果有副作用,调用方很难/不可能以纯功能的方式工作。这可能是问题,也可能不是问题,这取决于您的用例


我建议您有两种方法来获取迭代器-一种是有副作用的(基本上,这可能是一种优化),另一种是没有副作用的(速度较慢,但更易于推理)。这可能只是通过向方法传递一个标志,或者两个方法的名称不同。

迭代器是集合上的逻辑枚举,不应该有副作用。尤其是,当使用IEnumerator.Reset()方法重新启动时,迭代器不会是幂等的


然而,迭代器实际上是一种协同程序,这一事实对于实现某些难以用其他方式实现的事情是有用的,例如,

具有副作用的迭代器是不好的吗?:)

如果序列包含所有文件,则可以使用-ish访问所有项并为每种情况调用函数。访问者中的区别可以是作为谓词提供的,也可以是访问者中固有的

所以,我不会说C#,但类似于以下伪代码:

good_handler = new FileHandler() {
  handle(File f) { print "Yay!"; }
}

bad_handler = new FileHandler() {
  handle(File f) { print "Nay!"; }
}

files = YourFileSequence();
visitor = new Visitor(good_handler, bad_handler);
visitor.visit(files);

我的经验法则是,如果我在一个集合上进行迭代,则不会。但是在Python中,for循环通常习惯性地用于执行一定次数的代码,在这种情况下,我使用它时不会有任何副作用。

谢谢大家,天哪!快速反应

我不得不同意迭代器中的副作用是个坏主意。我不得不问的事实表明有一种气味。应该听我的蜘蛛感觉

我想我问这个问题的主要原因是因为我的副作用与主要任务是完全隔离的,因此被巧妙地封装在迭代器中。然而,它仍然是隐藏的功能,这不是很好

此外,我认为我将访问者的想法与迭代器混为一谈,这也不是一个好主意

从那以后,我改变了我的实现,从所有文件的原始序列生成了两个序列——一个好的,一个坏的。我现在可以用一种更加明显和直观的方式来处理它们。万岁

所以,我还没有在现实世界中使用迭代器。哦,好吧

谢谢!
马特

我想说副作用是个坏主意,但不是有害的。如果你有副作用,你基本上要做两个手术。最好将这些操作分为两个函数,这样代码更易于维护,并且可以分别执行


在这种情况下,您将坏文件移出文件夹,并将其他文件移到好文件中。通过分离这些操作,可以在不选择好文件的情况下移动坏文件,也可以在不移动坏文件的情况下对好文件进行操作(如计数)。您的代码也将更加划分,因此,如果需要,可以更容易地优化其中一个操作。

我认为,实际上,与迭代器具有隐藏的副作用这一事实相比,更直接的问题是。这就是:您正在更改它所迭代的集合的成员身份。即使副作用没有一种糟糕的代码味道,这也是你必须小心的事情。如果要从集合中删除内容,有一些方法可以实现这一点(比如,缓存文件列表并在重置迭代器时重新使用它),但这些方法可能会中断。

我不会对一般情况发表评论,但在您的情况下,我认为这是危险的。衡量接口质量的一个好指标是正确使用接口的容易程度和错误使用接口的难易程度

应用这个指标,你的设计得分很低,因为它很容易被错误地使用:只需迭代两次


事实上,我会比乔恩更进一步,说:甚至不要提供选择。这可能是有帮助的,但潜在地使用这个错误的代价可能太高了。另一方面,可以说,如果用户有意做出选择,他必须处理后果。

另一个问题是,该方法可能被“误用”,因为调用者可能试图使用它移动文件,而对返回的结果并不感兴趣

如果调用方从未对结果进行迭代,那么(预期的)副作用就不会因为迭代器的延迟执行而被调用。甚至可能存在这样的场景:用户只对集合的一部分进行迭代,因此会对某些项执行副作用,而不是对所有项执行副作用


这篇文章讨论了这个问题:

IEnumerator.Reset()很少实现,请注意-并且从未在迭代器块中实现。当然-但它是API的一部分这一事实表明,枚举是稳定的,重新启动是幂等的这是一个逻辑期望。它是API的一部分这一事实基本上是一个错误:)但是是的,我认为