【问题标题】:How can I programmatically detect side effects (compile time or run time)?如何以编程方式检测副作用(编译时或运行时)?
【发布时间】:2010-11-14 18:13:08
【问题描述】:

我有一个缓存的想法,我正在开始实施:

Memoizing functions 并将返回值与函数签名的哈希一起存储在Velocity 中。使用PostSharp,我想检查缓存并返回返回值的再水化表示,而不是再次调用该函数。我想使用属性来控制这种行为。

不幸的是,如果我的组织中的其他开发人员爱上了性能提升并开始使用缓存属性装饰每一个可见的方法,包括一些具有副作用的方法,这对我组织中的其他开发人员来说可能是危险的。当记忆库怀疑某个函数可能会导致副作用时,我想退出编译器警告。

我如何判断代码使用 CodeDom 或 Reflection 可能会导致副作用?

【问题讨论】:

  • @Chris:除非分析表明存在可以修复的瓶颈,否则没有人应该考虑使用您的优化。简而言之,思考你的函数的正确方法是,“嘿,东西很慢。我敢打赌记忆化会有所帮助。等等,克里斯为此写了一些代码!”而不是“嘿,克里斯写了一些代码来让事情变得更快。让我们使用它!”

标签: c# memoization side-effects


【解决方案1】:

简单地说,你不能使用 CodeDom 或 Reflection。

要准确确定某个方法是否会产生副作用,您必须了解它所采取的操作。对于 .Net,这意味着打开 IL 并以某种方式对其进行干预。

Reflection 或 CodeDom 都没有为您提供此功能。

  • CodeDom 是一种将代码生成到应用程序中的方法,其检查功能非常有限。它本质上仅限于各种解析引擎所理解的语言子集。
  • 反射的优势在于它能够检查元数据,而不是方法体的底层 IL。元数据只能为您提供一组非常有限的信息,说明什么会引起副作用,什么不会引起副作用。

【讨论】:

  • “元数据只能为您提供非常有限的一组信息,说明什么会引起副作用,什么不会引起副作用。”有什么例子吗?
  • @Chris,查找 DllImport 和其他一些项目会告诉您方法是 PInvoke 方法,因此必须假定具有副作用。
【解决方案2】:

反射本身不会这样做,因为元数据没有任何此类属性。

CodeDom 可能不够强大,无法检查所有 IL 指令。

因此,您必须使用反射 API 的非常低级的部分,让您获得包含每个方法的原始 IL 的 byte[],并对其进行分析。所以原则上是可能的,但并不容易。

您必须分析所有指令并观察它们有什么效果,以及这些效果是否会在某些重要范围之外继续存在(例如,它们是否修改了可以通过返回值或@ 987654322@ 参数,或者它们只是修改了保证在方法之外无法访问的瞬态对象?)。

听起来很复杂!

【讨论】:

  • 听起来确实很复杂,不是吗:(如果必须的话,我会满足于字符串解析,使用正则表达式查找关键字或其他文本结构。不一定是完美,但我认为至少尝试通过记忆函数来警告开发人员他们可能会做出错误的选择是个好主意。
  • 我严重怀疑正则表达式是否强大到足以在任何有用的程度上区分纯 C# 函数和副作用方法。请记住,您需要知道 BCL 中的哪些 API 具有副作用。 Console.WriteLine 确实如此。然后在某些情况下,函数在给定相同参数(或无参数)的情况下返回不同的值,例如Random.NextDateTime.Now - 你不想缓存他们的结果。还有多少其他例子?我不知道...我认为这是当今 .NET 框架和 BCL 中的主要漏洞,希望它会成为第 5 版的重点。
【解决方案3】:

这是一个非常困难的问题,无论是在实践中还是在理论上。我们正在努力考虑如何防止或隔离针对您的场景的副作用——memoization、automatic parallelization 等等——但这很困难,而且我们距离 C# 的可行解决方案还很远。所以,没有承诺。 (如果你真的想消除副作用,可以考虑改用 Haskell。)

不幸的是,即使发生了奇迹并且您找到了一种方法来防止记忆具有副作用的方法,您仍然遇到了一些问题。考虑以下几点:

1) 如果你 memoize 一个本身调用 memoized 函数的函数怎么办?这是一个很好的情况,对吧?您希望能够编写记忆函数。但是记忆化有一个副作用:它将数据添加到缓存中!所以你马上就有了一个元问题:你想驯服副作用,但只有“坏”的副作用。你想鼓励的“好”,你想阻止的坏,很难区分它们。

2) 你打算如何处理异常?你能记住一个抛出异常的方法吗?如果是这样,它总是抛出相同的异常,还是每次都抛出一个新的异常?如果是前者,你打算怎么做?如果是后者,现在你有一个记忆函数,它在两个不同的调用上有两个不同的结果,因为抛出了两个不同的异常。异常可以被视为副作用;很难驯服异常。

3) 对于没有副作用但仍然是不纯方法的方法,你打算怎么办?假设您有一个 GetCurrentTime() 方法。这没有副作用;调用不会改变任何内容。但这仍然不是记忆化的候选者,因为任何两个调用都需要来产生不同的结果。 您不需要副作用检测器,您需要的是纯度检测器。

我认为你最好的办法是通过教育和代码审查来解决人类问题,而不是试图解决困难的技术问题。

【讨论】:

  • F# 也可以用相当纯粹的方式编写。
  • @Paul,但 F# 也无法确保纯度。正如 Eric 所说,考虑改用 Haskell...
猜你喜欢
  • 2014-04-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多