关于不变性影响的经验数据?

今天在课堂上,我的教授正在讨论如何组建课程。 该课程主要使用Java,我拥有比老师更多的Java经验(他来自C ++背景),所以我提到在Java中应该支持不变性。 我的教授让我certificate我的答案是正确的,我给出了我从Java社区听到的原因:

  1. 安全性(尤其是螺纹加工)
  2. 减少对象数量
  3. 允许某些优化(特别是对于垃圾收集器)

教授对我的发言提出质疑,他说他希望看到对这些好处的一些统计测量。 我引用了大量轶事证据,但即便在我这样做的时候,我也意识到他是对的:据我所知,还没有一项关于不变性是否真正提供它在实际代码中所承诺的好处的实证研究。 我知道它来自经验,但其他人的经历可能有所不同。

所以,我的问题是,是否有关于不变性对现实世界代码的影响的统计研究?

我想指出Effective Java中的第15项 。 不变性的价值在于设计(并不总是合适的 – 它只是一个很好的初步近似),并且从统计的角度来看很少有设计偏好,但我们已经看到了可变对象(日历,日期)已经非常糟糕,严重的替代品(JodaTime,JSR-310)选择了不变性。

在我看来,Java中不变性的最大优点是简单性。 如果该状态不能改变,则推理对象的状态变得更加简单。 这在multithreading环境中当然更为重要,但即使在简单的线性单线程程序中,它也可以使事情变得更容易理解。

有关更多示例,请参阅此页面 。

所以,我的问题是,是否有关于不变性对现实世界代码的影响的统计研究?

我认为你的教授只是迟钝 – 不一定是有意甚至是坏事。 只是问题太模糊了。 这个问题有两个真正的问题:

  • “如果你没有指定你正在寻找什么样的测量,那么关于[x]效应的统计研究并不意味着什么。
  • 除非您声明特定域名,否则“真实世界代码”并不具有任何意义。 真实世界的代码包括科学计算,游戏开发,博客引擎,自动校对生成器,存储过程,操作系统核心等

为了它的价值,编译器优化不可变对象的能力已被充分记录。 脱离我的头顶:

  • Haskell编译器执行砍伐森林 (也称为快捷融合),其中Haskell将转换表达式map f . map g map f . map g map f . gmap f . g map f . g 。 由于Haskell函数是不可变的,因此保证这些表达式产生等效输出,但第二个函数运行速度是原来的两倍,因为我们不需要创建中间列表。
  • 公共子表达式消除我们可以转换x = foo(12); y = foo(12) x = foo(12); y = foo(12)temp = foo(12); x = temp; y = temp; temp = foo(12); x = temp; y = temp; 只有编译器可以保证foo是纯函数才有可能。 据我所知,D编译器可以使用pureimmutable关键字执行这样的替换。 如果我没记错的话,一些C和C ++编译器会积极地优化对这些标记为“纯”的函数的调用(或者等效的关键字)。
  • 只要我们没有可变状态,一个足够智能的编译器就可以执行多个线程的线性代码块,并保证我们不会破坏另一个线程中的变量状态。

关于并发性,使用可变状态的并发性缺陷已有详细记录,无需重述。


当然,这都是轶事证据,但这几乎是你得到的最好的证据。 不可改变与可变争论在很大程度上是一场小便,你不会发现一篇论文,如“function编程优于命令式编程”这样的全面概括。

最多 ,您可能会发现,您可以在一组最佳实践中总结不可变与可变的好处,而不是编码的研究和统计。 例如,可变状态是multithreading编程的敌人; 另一方面,可变队列和数组通常比它们的不可变变体更容易编写并且在实践中更有效。

这需要练习,但最终你会学会使用正确的工具来完成工作,而不是将你最喜欢的宠物范例用于项目。

我认为你的教授过于顽固(可能是刻意的,是为了让你更全面地理解)。 实际上,不可变性的好处并不在于编译器能够对优化做些什么,但实际上,对于我们人类而言,阅读和理解更容易。 保证在创建对象时保持设置并且保证之后不会更改的变量比现在的值更容易理解和推理,但稍后可能设置为某个其他值。

对于线程来说尤其如此,因为当语言保证不会发生这样的修改时,您不需要担心处理器缓存和监视器以及避免并发修改所带来的所有样板。

一旦你将不变性的好处表达为“代码更易于遵循”,就要求对“更容易遵循”的生产率提高进行经验测量会感觉有点愚蠢。

另一方面,编译器和Hotspot可能会基于知道值永远不会改变而执行某些优化 – 就像你我感觉这会发生并且是一件好事但我不确定细节。 很可能会有可能发生的优化类型的经验数据,以及生成的代码的速度。

  • 不要和教授争论。 你没什么可收获的。
  • 这些是开放式问题,如动态和静态类型。 我们有时认为涉及不可变数据的function技术由于各种原因更好,但到目前为止它主要是风格问题。

你会客观地测量什么? 可以使用相同程序的可变/不可变版本来测量GC和对象计数(尽管那是多么典型的主观,因此这是一个相当弱的论点)。 我无法想象你如何能够测量线程错误的消除,除非通过与生成应用程序的现实世界示例进行比较,通过添加不变性来解决间歇性问题。

不可变性对于价值对象来说是一件好事。 但其他事情怎么样? 想象一个创建统计数据的对象:

 Stats s = new Stats (); ... some loop ... s.count (); s.end (); s.print (); 

应该打印“Processed 536.21 rows / s”。 你打算如何使用不可变的方法实现count() ? 即使您为计数器本身使用不可变值对象, s也不能是不可变的,因为它必须替换自身内部的计数器对象。 唯一的出路是:

  s = s.count (); 

这意味着复制循环中每一轮的s状态。 虽然这可以做到,但它肯定不如递增内部计数器那么有效。

此外,大多数人都不能正确使用这个API,因为他们希望count()能够修改对象的状态而不是返回一个新的状态。 所以在这种情况下,它会产生更多错误。

正如其他评论所声称的那样,收集不可变对象优点的统计数据非常非常困难,因为找到控制案例几乎是不可能的 – 各种软件应用程序在各方面都是相似的,除了一个使用不可变的对象和另一个没有。 (几乎在所有情况下,我都声称该软件的一个版本是在一段时间之后编写的,并从第一版学到了许多经验教训,因此性能的提高会有很多原因。)任何有经验的程序员都会考虑这个问题。片刻应该意识到这一点。 我想你的教授正试图转移你的建议。

同时,很容易做出有利于不变性的有力论据,至少在Java中,可能在C#和其他OO语言中。 正如Yishai所说, Effective Java很好地certificate了这一观点。 Java Concurrency in Practice的副本也在我的书架上。

不可变对象允许通过共享引用来共享对象的代码。 但是,可变对象具有想要共享对象标识的代码的标识 ,通过共享引用来实现。 在大多数应用中,两种共享都是必不可少的。 如果没有可用的不可变对象,则可以通过将值复制到由这些值的预期接收者提供的新对象或对象来共享值。 让我没有可变对象要困难得多。 可以通过说stateOfUniverse = stateOfUniverse.withSomeChange(...)来某种“伪造”可变对象,但是当它的withSomeChange方法正在运行时会要求没有其他任何修改stateOfUniverse [排除任何类型的multithreading]。 此外,如果一个人试图跟踪一队卡车,并且部分代码对一辆特定的卡车感兴趣,则该代码必须始终在卡车的桌子上随时查看该卡车。 。

更好的方法是将Universe细分为实体和值。 实体将具有可变的特征,但具有不可变的特性,因此例如Truck类型的存储位置可以继续识别同一卡车,即使卡车本身改变位置,装载和卸载货物等等。值通常不具有特定的标识,但会有不可改变的特征。 Truck可能会将其位置存储为WorldCoordinate类型。 代表45.6789012N 98.7654321W的WorldCoordinate将继续如此,只要有任何提及它; 如果位于该位置的卡车略微向北移动,它将创建一个新的WorldCoordinate来代表45.6789013N 98.7654321W,放弃旧的,并存储对该新卡车的引用。

当一切都封装了不可变值或不可变身份时,以及当应该具有不可变身份的事物是可变的时,通常最容易推理代码。 如果一个人不想在变量stateOfUniverse之外使用任何可变对象,更新卡车的位置将需要以下内容:

 ImmutableMapping trucks = stateOfUniverse.getTrucks(); Truck myTruck = trucks.get(myTruckId); myTruck = myTruck.withLocation(newLocation); trucks = trucks.withItem(myTruckId,myTruck); stateOfUniverse = stateOfUniverse.withTrucks(trucks); 

但是对该代码的推理会比以下更难:

 myTruck.setLocation(newLocation);