如果不可变对象¹很好,简单并且在并发编程中提供了好处,那么为什么程序员会继续创建可变对象²?

我在Java编程方面有四年的经验,正如我所见,第一个人们创建类后要做的事情是在IDE中生成getter和setter(因此使其可变)。在大多数情况下是否缺乏认识或我们可以摆脱使用可变对象的原因?


¹不可变对象是创建后状态无法修改的对象。 >²可变对象是创建后可以修改的对象。

评论

我认为,除了正当理由(如下文Péter所述)外,“懒惰的开发人员”比“愚蠢的开发人员”更常见。在“愚蠢的开发人员”之前,还有“不知情的开发人员”。

对于每位福音派程序员/博客作者,都有1000位热情的博客读者,他们立即重新发明自己并采用最新技术。对于其中的每一个,那里有10,000名程序员,他们的鼻子到磨石,一天的工作就完成了,将产品推到了门外。这些家伙正在使用久经考验的可信赖技术。他们等到新技术被广泛采用并显示出实际收益后再采用它们。不要称它们为愚蠢,它们只是懒惰,而是称它们为“忙”。

@BinaryWorrier:不变的对象几乎不是“新事物”。它们可能没有大量用于域对象,但是Java和C#从一开始就拥有它们。另外:“懒惰”并不总是一个坏词,对于开发人员来说,某些“懒惰”绝对是优势。

@Joachim:我认为在上面的贬义中使用“懒惰”是很明显的:)另外,不可变对象(例如Lambda微积分和OOP早在今天-是的,我已经很老了)不需要是新的突然成为本月的风味。我并不是在说他们不是一件坏事(不是),或者他们没有自己的位置(显然是他们),只是对人们轻松一点,因为他们没有听过最新的《好话》,被热切地转化为自己(不要责怪您的“懒惰”评论,我知道您试图减轻它)。

-1,不可变对象arent'good'。只是或多或少适合于特定情况。在所有情况下,任何人告诉您一种技术或另一种技术在客观上都比另一种技术“好”或“坏”是在向您兜售宗教信仰。

#1 楼

可变和不变的对象都有各自的用途和优点和缺点。

不变的对象确实在很多情况下确实使生活更简单。它们特别适用于值类型,其中对象没有身份,因此可以轻松替换它们。而且它们可以使并发编程的方式更安全,更干净(众所周知,并发错误中的大多数很难找到,并发错误最终是由线程之间共享的可变状态引起的)。但是,对于大型和/或复杂的对象,为每个单个更改创建对象的新副本可能会非常昂贵和/或乏味。对于具有独特身份的对象,更改现有对象比创建新的修改后的副本更加简单直观。

思考游戏角色。在游戏中,速度是重中之重,因此与可变对象代表游戏角色相比,替代方案可能会为每一个小小的变化生成新的游戏角色副本,从而使游戏的运行速度大大提高。

此外,我们对现实世界的认识不可避免地基于可变对象。当您在加油站给汽车加油时,您始终将其视为同一对象(即,其身份在状态改变时得以保留)-好像旧的空油罐车被连续的新油罐车所取代坦克越来越满的汽车实例。因此,每当我们在程序中为某些真实世界建模时,使用可变对象来代表真实世界实体通常更直接,更轻松地实现领域模型。

除了所有这些合理的理由外,a,人们不断创造可变物体的最可能原因是思维的惯性,也就是对变化的抵制。请注意,当今的大多数开发人员在不变性(以及包含的范式,函数式编程)在他们的影响范围内变得“时髦”之前就已经接受了良好的培训,并且不让他们对我们的交易的新工具和方法保持最新​​的知识-实际上,我们许多人都积极抵制新观念和新进程。 “我已经这样编程了nn年了,我不在乎最新的愚蠢时尚!”

评论


那就对了。特别是在GUI编程中,可变对象非常方便。

–弗洛里安·萨利霍维奇(Florian Salihovic)
2012年6月6日7:56

这不仅仅是阻力,我敢肯定,许多开发人员都愿意尝试最新和最出色的技术,但是新项目在普通开发人员的环境中可以应用这些新实践的频率如何?不是每个人都能或都会编写一个爱好项目只是为了尝试一成不变的状态。

–史蒂文·埃弗斯(Steven Evers)
2012年6月6日14:18

有两个小警告:(1)以移动游戏角色为例。例如,.NET中的Point类是不可变的,但是由于更改而创建新的点很容易,因此可以承受。通过解耦“活动部分”,可以使制作不可变角色的动画变得非常便宜(但是,是的,某些方面是可变的)。 (2)“大型和/或复杂的对象”很可能是不可变的。字符串通常很大,通常会受益于不变性。我曾经将复杂的图形类重写为不可变的,从而使代码更简单,更高效。在这种情况下,具有可变的构建器是关键。

–康拉德·鲁道夫(Konrad Rudolph)
2012年6月6日14:23



@KonradRudolph,很好,谢谢。我并不是要排除在复杂对象中使用不变性,而是要正确而有效地实现此类并非易事,而且不一定总能证明需要付出额外的努力。

–彼得·托克(PéterTörök)
2012年6月6日14:47

您对状态与身份提出了很好的观点。这就是为什么Rich Hickey(Clojure的作者)在Clojure中将两者分开的原因。有人可能会争辩说,您拥有1/2汽油箱的汽车与拥有1/4汽油箱的汽车是不同的。它们具有相同的身份,但又不相同,我们现实时间的每一个“滴答声”都会创造出我们世界上每个对象的克隆,然后我们的大脑将它们用共同的身份简单地缝合在一起。 Clojure具有引用,原子,代理等来表示时间。以及实际时间的地图,向量和列表。

–提莫西·鲍德里奇(Timothy Baldridge)
2012年6月6日18:56

#2 楼

我想你们都错过了最明显的答案。大多数开发人员创建可变对象,因为可变性是命令式语言的默认设置。与不断修改远离默认值的代码(不管是否正确)相比,我们大多数人与我们的时间关系更好。不变性不是任何其他方法的灵丹妙药。正如某些答案所指出的那样,它使某些事情变得容易,但使其他事情变得更加困难。

评论


在我所知的大多数现代IDE中,仅生成吸气剂与生成吸气剂和设置器都需要花费几乎相同的精力。尽管添加final,const等确实是需要付出额外的努力的……但是,除非您设置了代码模板:-)

–彼得·托克(PéterTörök)
2012年6月7日在7:19



@PéterTörök这不仅是额外的工作-这是您的编码同伴想挂在您身上的事实,因为他们发现您的编码风格与他们的经验格格不入。这也阻止了这种事情。

–奥诺里奥(Atorio Catenacci)
2012年6月7日12:37

这可以通过更多的交流和教育来克服,例如在实际将其引入现有代码库之前,建议先与队友讨论或介绍新的编码样式。的确,一个好的项目具有通用的编码风格,而不仅仅是每个(过去和现在)项目成员都喜欢的编码风格的融合。因此,引入或更改编码习惯用法应该是团队的决定。

–彼得·托克(PéterTörök)
2012年6月7日13:08

@PéterTörök在命令式语言中,可变对象是默认行为。如果只需要不可变的对象,则最好切换到功能语言。

–emory
2012年6月8日,1:15

@PéterTörök假定将不变性纳入程序中只涉及丢弃所有设置者,这有点天真。您仍然需要一种使程序状态逐渐变化的方法,并且为此,您需要构建器,冰棒不可变性或可变代理,所有这些构建器或组件的耗用大约是丢弃设置者所花费的十亿倍。

–阿萨德·塞德丁(Asad Saeeduddin)
15年8月1日在6:44

#3 楼


这里有可变性的地方。领域驱动的设计原则对什么应该是可变的和什么是不可变的提供了扎实的理解。如果您考虑一下,您将意识到构想一个系统,在该系统中,对对象的每次状态更改都需要销毁它,并对引用它的每个对象进行重新组合,这是不切实际的。对于复杂的系统,这很容易导致完全擦除和重建整个系统的对象图
大多数开发人员不会在性能要求非常重要以至于需要专注于并发性(或许多其他问题)上做任何事情。被知情人士普遍认为是好的做法)。

有些事情是您无法使用不可变对象完成的,例如具有双向关系。一旦在一个对象上设置了关联值,它的身份就会改变。因此,您在另一个对象上设置了新值,并且它也发生了变化。问题在于第一个对象的引用不再有效,因为已经创建了一个新实例来用引用表示该对象。继续这样做只会导致无限回归。阅读您的问题后,我做了一些案例研究,这是什么样子。您是否有其他方法可以在保持不变性的同时允许此类功能?

    public class ImmutablePerson { 

     public ImmutablePerson(string name, ImmutableEventList eventsToAttend)
     {
          this.name = name;
          this.eventsToAttend = eventsToAttend;
     }
     private string name;
     private ImmutableEventList eventsToAttend;

     public string Name { get { return this.name; } }

     public ImmutablePerson RSVP(ImmutableEvent immutableEvent){
         // the person is RSVPing an event, thus mutating the state 
         // of the eventsToAttend.  so we need a new person with a reference
         // to the new Event
         ImmutableEvent newEvent = immutableEvent.OnRSVPReceived(this);
         ImmutableEventList newEvents = this.eventsToAttend.Add(newEvent));
         var newSelf = new ImmutablePerson(name, newEvents);
         return newSelf;
     }
    }

    public class ImmutableEvent { 
     public ImmutableEvent(DateTime when, ImmutablePersonList peopleAttending, ImmutablePersonList peopleNotAttending){
         this.when = when;     
         this.peopleAttending = peopleAttending;
         this.peopleNotAttending = peopleNotAttending;
     }
     private DateTime when; 
     private ImmutablePersonList peopleAttending;
     private ImmutablePersonList peopleNotAttending;
     public ImmutableEvent OnReschedule(DateTime when){
           return new ImmutableEvent(when,peopleAttending,peopleNotAttending);
     }
     //  notice that this will be an infinite loop, because everytime one counterpart
     //  of the bidirectional relationship is added, its containing object changes
     //  meaning it must re construct a different version of itself to 
     //  represent the mutated state, the other one must update its
     //  reference thereby obsoleting the reference of the first object to it, and 
     //  necessitating recursion
     public ImmutableEvent OnRSVPReceived(ImmutablePerson immutablePerson){
           if(this.peopleAttending.Contains(immutablePerson)) return this;
           ImmutablePersonList attending = this.peopleAttending.Add(immutablePerson);
           ImmutablePersonList notAttending = this.peopleNotAttending.Contains( immutablePerson ) 
                                ? peopleNotAttending.Remove(immutablePerson)
                                : peopleNotAttending;
           return new ImmutableEvent(when, attending, notAttending);
     }
    }
    public class ImmutablePersonList
    {
      private ImmutablePerson[] immutablePeople;
      public ImmutablePersonList(ImmutablePerson[] immutablePeople){
          this.immutablePeople = immutablePeople;
      }
      public ImmutablePersonList Add(ImmutablePerson newPerson){
          if(this.Contains(newPerson)) return this;
          ImmutablePerson[] newPeople = new ImmutablePerson[immutablePeople.Length];
          for(var i=0;i<immutablePeople.Length;i++)
              newPeople[i] = this.immutablePeople[i];
          newPeople[immutablePeople.Length] = newPerson;
      }
      public ImmutablePersonList Remove(ImmutablePerson newPerson){
          if(immutablePeople.IndexOf(newPerson) != -1)
          ImmutablePerson[] newPeople = new ImmutablePerson[immutablePeople.Length-2];
          bool hasPassedRemoval = false;
          for(var i=0;i<immutablePeople.Length;i++)
          {
             hasPassedRemoval = hasPassedRemoval || immutablePeople[i] == newPerson;
             newPeople[i] = this.immutablePeople[hasPassedRemoval ? i + 1 : i];
          }
          return new ImmutablePersonList(newPeople);
      }
      public bool Contains(ImmutablePerson immutablePerson){ 
         return this.immutablePeople.IndexOf(immutablePerson) != -1;
      } 
    }
    public class ImmutableEventList
    {
      private ImmutableEvent[] immutableEvents;
      public ImmutableEventList(ImmutableEvent[] immutableEvents){
          this.immutableEvents = immutableEvents;
      }
      public ImmutableEventList Add(ImmutableEvent newEvent){
          if(this.Contains(newEvent)) return this;
          ImmutableEvent[] newEvents= new ImmutableEvent[immutableEvents.Length];
          for(var i=0;i<immutableEvents.Length;i++)
              newEvents[i] = this.immutableEvents[i];
          newEvents[immutableEvents.Length] = newEvent;
      }
      public ImmutableEventList Remove(ImmutableEvent newEvent){
          if(immutableEvents.IndexOf(newEvent) != -1)
          ImmutableEvent[] newEvents = new ImmutableEvent[immutableEvents.Length-2];
          bool hasPassedRemoval = false;
          for(var i=0;i<immutablePeople.Length;i++)
          {
             hasPassedRemoval = hasPassedRemoval || immutableEvents[i] == newEvent;
             newEvents[i] = this.immutableEvents[hasPassedRemoval ? i + 1 : i];
          }
          return new ImmutableEventList(newPeople);
      }
      public bool Contains(ImmutableEvent immutableEvent){ 
         return this.immutableEvent.IndexOf(immutableEvent) != -1;
      } 
    }




评论


@AndresF。,如果您对如何仅使用不可变对象维护具有双向关系的复杂图有不同的看法,那么我很想听听。 (我假设我们可以同意一个集合/数组是一个对象)

–smartcaveman
2012年6月7日在3:36

@AndresF。,(1)我的第一个陈述不是通用的,因此不是虚假的。我实际上提供了一个代码示例来解释在某些情况下(在应用程序开发中很常见)它是如何正确的。 (2)通常,双向关系需要可变性。我不认为Java是罪魁祸首。正如我所说,我很乐意评估您提出的任何建设性替代方案,但在这一点上,您的评论听起来很像“您错了,因为我这么说”。

–smartcaveman
2012年6月7日下午16:31

@smartcaveman关于(2),我也不同意:通常,“双向关系”是与可变性正交的数学概念。正如通常在Java中实现的那样,它确实需要可变性(我在这一点上同意您的观点)。但是,我可以想到一个替代实现:两个对象之间的Relationship类,带有构造函数Relationship(a,b);在创建关系时,两个实体a和b已经存在,并且关系本身也是不可变的。我并不是说这种方法在Java中是可行的。只是有可能。

– Andres F.
2012年6月7日在17:21



@AndresF。,因此,根据您的意思,如果R是Relationship(a,b),并且a和b都是不可变的,则a和b都不持有对R的引用。将必须存储在其他地方(例如静态类)。我是否正确理解您的意图?

–smartcaveman
2012年6月7日18:38

Chthulhu通过懒惰指出,可以为不可变数据存储双向关系。这是执行此操作的一种方法:haskell.org/haskellwiki/Tying_the_Knot

–托马斯·埃丁
2012年8月24日23:36

#4 楼

我一直在阅读“纯功能数据结构”,这让我意识到,有很多数据结构更容易使用可变对象来实现。

要实现二进制搜索树,您每次必须返回一棵新树:您的新树将必须为每个已修改的节点制作一个副本(共享未修改的分支)。对于您的插入功能来说,还算不错,但是对我来说,当我开始进行删除和重新平衡时,事情变得非常低效。

要实现的另一件事是,您可以多年编写面向对象的代码,如果您的代码未以暴露并发性问题的方式运行,则永远不会真正意识到共享可变状态的可怕性。

评论


这是冈崎的书吗?

–机械蜗牛
2012年6月7日在3:01

是的有点干,但是有很多很好的信息...

– Paul Sanwald
2012年6月7日13:21

有趣的是,我一直以为冈崎的红/黑树要简单得多。 10行左右。我猜最大的优势是当您实际上还希望保留旧版本时。

–托马斯·阿勒
2012年6月15日下午13:43

尽管最后一句话可能在过去是正确的,但鉴于当前的硬件趋势等,尚不清楚它是否会在将来保持正确。

– jk。
14年2月13日在13:42

#5 楼

从我的角度来看,这是缺乏认识的。如果您查看其他已知的JVM语言(Scala,Clojure),则在代码中很少看到可变对象,这就是为什么人们在单线程不够的情况下开始使用它们的原因。
我目前学习Clojure并在Scala上有一点经验(也有4年以上Java经验),并且由于对状态的了解,您的编码风格也发生了变化。

评论


也许用“已知”而不是“受欢迎”是一个更好的选择。

–丹
2012年6月6日上午8:32

是的,这是对的。

–弗洛里安·萨利霍维奇(Florian Salihovic)
2012年6月6日上午8:33

+1:我同意:在学习了一些Scala和Haskell之后,我倾向于在Java中使用final在Java中使用const,在C ++中使用const。如果可能,我还会使用不可变对象,尽管仍然经常需要可变对象,但令人惊奇的是您经常使用不可变对象。

–乔治
2012年8月17日在16:19



两年半后,我阅读了我的这篇评论,但我的观点已经改变,以支持不变性。在我当前的项目(在Python中)中,我们很少使用可变对象。甚至我们的持久数据也是不可变的:由于某些操作,我们会创建新记录,并在不再需要旧记录时删除旧记录,但是我们永远不会更新磁盘上的任何记录。不用说,这使我们目前并发的多用户应用程序更易于实现和维护。

–乔治
2014年12月9日在21:51

#6 楼

我认为一个主要的促成因素已被忽略:Java Beans严重依赖于一种特定的突变对象样式,并且(尤其是考虑到源代码)很多人似乎将其视为所有Java如何(甚至)的典型示例。应该写。

评论


+1,在第一次数据分析之后,使用getter / setter模式作为某种默认实现的方式就太多了。

– Jaap
2012年6月7日12:12

这可能是一个重点,“因为其他人正在这样做”,所以它一定是正确的。对于“ Hello World”程序,这可能是最简单的。通过一系列可变属性来管理对象状态变化……比“ Hello World”理解的深度要难一些。 20年后,我非常惊讶于20世纪编程的顶峰是在没有任何结构的对象的每个属性上编写getX和setX方法(多么乏味)。距离直接访问具有100%可变性的公共财产仅一步之遥。

–达雷尔·蒂格(Darrell Teague)
13 Mar 15 '13 at 20:53

#7 楼

我在职业生涯中使用过的每个企业Java系统都使用Hibernate或Java Persistence API(JPA)。 Hibernate和JPA本质上要求您的系统使用可变对象,因为它们的全部前提是它们检测并保存对数据对象的更改。对于许多项目而言,Hibernate带来的易于开发性比不可变对象的优势更具吸引力。

显然,可变对象的存在时间比Hibernate要长得多,因此Hibernate可能不是最初的“原因”易变对象的普及。

但是今天,如果许多初级程序员在使用Hibernate或另一个ORM的企业系统上cut之以鼻,那么大概他们会养成使用可变对象的习惯。像Hibernate这样的框架可能正在巩固可变对象的流行。

评论


优点。要成为所有人的万物,这些框架别无选择,只能落入最低的公分母,使用基于反射的代理并获取/设置其灵活性的方式。当然,这会创建几乎没有状态转换规则的系统,或者不幸的是通过一种通用的方法来实施它们。在许多项目完成后,我不确定要在权宜性,可扩展性和正确性方面做得更好。我倾向于认为是混合动力车。动态ORM优缺点,但具有一些定义,其中要求哪些字段是必需的以及应该可以进行哪些状态更改。

–达雷尔·蒂格(Darrell Teague)
2013年3月15日20:57



#8 楼

尚未提及的主要要点是,使对象的状态可变是可以使封装该状态的对象的身份不可变的。

许多程序旨在模拟现实世界本质上是易变的东西。假设在12:51 am,某个变量AllTrucks拥有对对象#451的引用,该对象是数据结构的根,该数据结构指示当时(12:51 am)一个车队的所有卡车中都装有什么货物。变量BobsTruck可用于获取对对象#24601的引用,该对象指向一个对象,该对象指示当时(12:51 am)鲍勃卡车中所装的货物。在12:52 am,一些卡车(包括Bob的卡车)被装卸,并且数​​据结构被更新,因此AllTrucks现在将保留对数据结构的引用,该数据结构指示截至12:52 am的所有卡车中的货物。 />
BobsTruck应该怎么办?

如果每个卡车对象的'cargo'属性都是不可变的,则对象#24601将永远代表鲍勃卡车在上午12:51处的状态。如果BobsTruck直接引用对象#24601,则除非更新AllTrucks的代码也恰好更新BobsTruck,否则它将不再代表鲍勃卡车的当前状态。还要注意,除非以某种形式的可变对象存储BobsTruck,否则更新AllTrucks的代码唯一可以更新的方法是对代码进行显式编程。

如果一个人希望能够使用BobsTruck来观察鲍勃卡车的状态,同时仍然使所有对象保持不变,则可以使BobsTruck是一个不变的函数,给定AllTrucks在任何特定时间具有或具有的值,它将产生的状态。当时鲍勃的卡车。一个人甚至可能拥有一对不可变的功能-其中一个与上面相同,另一个将接受对车队状态和新卡车状态的引用,并返回对新车队状态的引用

不幸的是,每次有人想要访问鲍勃卡车的状态时都必须使用这种功能会很烦人和麻烦。一种替代方法是说对象#24601将永远(只要有人持有对它的引用)代表鲍勃卡车的当前状态。想要重复访问Bob卡车当前状态的代码不必每次都运行一些耗时的功能-它只需执行一次查找功能就可以发现对象#24601是Bob的卡车,然后只需想要查看Bob卡车的当前状态时,可以随时访问该对象。

请注意,在单线程环境或多线程环境中,函数方法并非没有优势,在多线程环境中,线程通常只会观察数据而不是更改数据。任何观察者线程复制AllTrucks中包含的对象参考,然后检查代表的卡车状态,从其抓取参考的那一刻起,便会看到所有卡车的状态。每当观察者线程想要查看更新的数据时,它都可以重新获取引用。另一方面,用单个不变对象表示车队的整个状态将排除两个线程同时更新不同卡车的可能性,因为每个线程如果留给自己的设备将产生一个新的“车队状态”对象,其中包括卡车的新状态和其他旧状态。如果每个线程仅在未更改的情况下使用CompareExchange更新AllTrucks,并通过重新生成其状态对象并重试该操作来响应失败的CompareExchange,则可以确保正确性,但是如果多个线程尝试同时进行写操作,则性能会得到保证。通常比所有写操作都在单个线程上执行要差;尝试进行此类同时操作的线程越多,性能将越差。

如果单个卡车对象是可变的,但具有不变的标识,则多线程方案将变得更干净。在任何给定的卡车上一次只能允许一个线程运行,但是在不同卡车上运行的线程可以这样做而不会受到干扰。尽管即使使用不可变的对象,也有很多方法可以模仿这种行为(例如,可以定义“ AllTrucks”对象,以便将属于XXX的卡车的状态设置为SSS,仅需要生成一个表示“截至[时间],属于[XXX]的卡车的状态现在为[SSS];其他所有状态为[AllTrucks的旧值]。生成这样的对象将足够快,以至于即使存在争用,CompareExchange循环也不会另一方面,使用这种数据结构将大大增加找到特定人员的卡车所需的时间。使用具有不变身份的可变对象可以避免该问题。

#9 楼

没有对与错,这取决于您的喜好。有一个原因为什么有些人偏爱使用一种范例而不是另一种范例,以及使用一种数据模型而不是另一种范例的语言。这仅取决于您的喜好以及要实现的目标(并且能够轻松使用两种方法而不会疏远一方或另一方的顽固支持者是某些语言所追求的圣杯)。

我认为,回答您问题的最好,最快的方法是让您掌握不可变与可变性的优缺点。

#10 楼

检查此博客文章:http://www.yegor256.com/2014/06/09/objects-should-be-immutable.html。总结了为什么不变的对象比可变的对象更好。这是参数的简短列表:


不可变对象更易于构造,测试和使用
真正不可变对象始终是线程安全的
,它们有助于避免时间耦合
它们的用法没有副作用(没有防御性副本)
避免了身份变异性问题
它们总是具有故障原子性
它们更容易缓存

据我了解,人们正在使用可变对象,因为它们仍将OOP与命令式程序编程混合在一起。

#11 楼

在Java中,不可变的对象需要一个构造函数,该构造函数将采用该对象的所有属性(或者该构造函数从其他参数或默认值创建它们)。这些属性应标记为final。

这主要与数据绑定有四个问题:


Java构造函数反射元数据不保留参数名称。
Java构造函数(和方法)没有命名参数(也称为标签),因此会与许多参数混淆。
当继承另一个不可变对象时,必须调用正确的构造函数顺序。只是放弃并保留其中一个字段为非最终字段,这可能非常棘手。
大多数绑定技术(例如Spring MVC数据绑定,Hibernate等)仅适用于no-arg。默认构造函数(这是因为注释并不总是存在)。

您可以使用@ConstructorProperties之类的注释来减轻#1和#2的压力,并创建另一个可变的生成器对象(通常是流畅的)来创建不可变对象。

#12 楼

我很惊讶没有人提到性能优化的好处。根据语言的不同,编译器可以在处理不可变数据时进行一系列优化,因为它知道数据永远不会改变。跳过了各种各样的工作,这为您带来了巨大的性能优势。

不可变的对象几乎消除了整个状态缺陷类。

它并没有那么大是因为它更难,并不能适用于每种语言,而且大多数人都学会了命令式编码。

我还发现,大多数程序员对自己的想法感到满意,并且经常抵制他们不愿意采用的新思想不太了解。通常,人们不喜欢更改。

还要记住,大多数程序员的状态都是不好的。大多数在野外完成的编程都是可怕的,并且是由于缺乏理解和政治所致。

#13 楼

人们为什么使用任何强大的功能?人们为什么使用元编程,懒惰或动态类型化?答案是方便。可变状态是如此简单。它很容易就地更新,并且不可变状态比可变状态更有效地工作的项目规模的门槛很高,因此选择不会一会儿让您失望。

#14 楼

编程语言旨在供计算机执行。计算机的所有重要组成部分-CPU,RAM,缓存,磁盘-都是可变的。当它们不是(BIOS)时,它们实际上是不可变的,您也不能创建新的不可变对象。

因此,在不可变对象之上构建的任何编程语言都存在以下缺陷:它的实现。对于像C这样的早期语言,这是一个很大的绊脚石。

#15 楼

没有可变对象,您就没有状态。诚然,如果可以管理它,并且有可能从多个线程中引用一个对象,那么这是一件好事。但是该程序将相当无聊。许多软件,特别是Web服务器,通过在数据库,操作系统,系统库等上推销可变性来避免对可变对象负责。实际上,这确实使程序员摆脱了可变性问题并使Web(及其他)成为可能发展负担得起。但是,可变性仍然存在。

通常,您有三种类型的类:普通的,非线程安全的类,必须仔细地加以保护。不变的类,可以自由使用;以及可以自由使用的可变线程安全类,但必须格外小心。第一种是麻烦的,最坏的是被认为是第三种的。当然,第一种类型很容易编写。

我通常会遇到很多普通的,易变的类,我必须非常仔细地观察它们。在多线程情况下,即使我可以避免致命的拥抱,必要的同步也会减慢一切。因此,我通常在制作可变类的不变副本,并将其交给任何可以使用它的人。每次原始变异时,都需要一个新的不可变副本,因此我想有时我可能会有一百个原始副本。我完全依赖垃圾收集。

总之,如果您不使用多个线程,则非线程安全的可变对象会很好。 (但是到处都是多线程,请多加注意!)如果可以将它们限制为局部变量或严格同步它们,则可以安全地使用它们。如果可以通过使用他人的经过验证的代码(DB,系统调用等)来避免使用它们,则可以这样做。如果可以使用不可变的类,请这样做。而且我认为,总的来说,人们要么没有意识到多线程问题,要么(明智地)对它们感到恐惧,并使用各种技巧来避免多线程(或者将责任推到其他地方)。

作为PS,我感觉到Java获取器和设置器已失去控制。检查一下。

评论


不可变状态仍然是状态。

–杰里米·海勒(Jeremy Heiler)
2012年6月6日在22:01

@JeremyHeiler:是的,但这是可变的状态。如果没有任何变化,则只有一个状态,这与没有状态一样。

–RalphChapin
2012年6月7日在20:07

#16 楼

很多人都给出了很好的答案,所以我想指出的是您所观察到的一些内容非常真实,非常真实,在这里没有其他地方提及。

自动创建setter和getters太可怕了,太可怕了想法,但这是有程序意识的人们尝试将OO引入他们的思维方式的第一种方法。设置器和获取器以及属性仅应在您发现需要它们时创建,而不是默认情况下创建的

事实上,尽管您很定期地需要获取器,但是设置器或可写属性的唯一方式应该永远存在代码是通过构建器模式完成的,在该模式下,对象完全实例化后便被锁定。

许多类在创建后都是可变的,这很好,只是不应该直接对其属性进行操作-而是应该要求通过其中带有实际业务逻辑的方法调用来操纵其属性(是的,一个setter与直接操纵该属性几乎是一样的事情)

现在这不适用于可以使用“脚本”样式的代码/语言,但要编写代码是为他人创建的代码,并期望其他人在过去的几年中会反复阅读。我最近不得不开始做出这种区分,因为我非常喜欢与Groovy交往,而且目标之间存在巨大差异。

#17 楼

当实例化对象后必须设置倍数值时,将使用可变对象。

您不应具有带有六个参数的构造函数。相反,您可以使用setter方法来修改对象。

举一个例子,它是一个Report对象,其中包含用于字体,方向等的setter。

简而言之:可变变量在您使用时很有用

设置一个对象有很多状态,构造函数签名很长是不切实际的。

编辑:可以使用Builder模式来构建对象的整个状态。

评论


这似乎是可变性,实例化后的更改是设置多个值的唯一方法。为了完整起见,请注意,Builder模式提供了相同甚至更强大的功能,并且不需要牺牲不变性。新的ReportBuilder()。font(“ Arial”)。orientation(“ landscape”)。build()

– gna
2012年8月17日在13:12



“拥有非常长的构造函数签名是不现实的。”:您始终可以将参数分组为较小的对象,并将这些对象作为参数传递给构造函数。

–乔治
2012年8月17日在18:08

@cHao如果要设置10或15个属性怎么办?同样,名为withFont的方法返回Report也不是一件好事。

–图兰斯·科尔多瓦(TulainsCórdova)
2012年12月27日在17:10

就设置10或15个属性而言,如果(1)Java知道如何消除所有这些中间对象的构造,并且如果(2)再次将名称标准化,那么这样做的代码就不会感到尴尬。不变性不是问题; Java不知道如何做好是一个问题。

– cHao
2012-12-27 17:44



@cHao为20个属性建立调用链是很丑陋的。丑陋的代码往往是劣质代码。

–图兰斯·科尔多瓦(TulainsCórdova)
2012年12月27日在17:45

#18 楼

我认为使用可变对象源于命令式思维:通过逐步更改可变变量的内容(副作用计算)来计算结果。

如果从功能上考虑,您希望具有不变的状态并通过应用功能并从旧功能中创建新值来表示系统的后续状态。

功能性方法可以更简洁,更可靠,但是由于复制效率很低,因此您需要回退到您要逐步修改的共享数据结构。

我认为最合理的权衡是:从不可变对象开始,如果实现不够快,则切换到可变对象。从这个角度来看,从一开始就系统地使用可变对象可以被视为某种过早的优化:您从一开始就选择了更有效(但也更难以理解和调试)的实现。

那么,为什么许多程序员使用可变对象?恕我直言,有两个原因:


许多程序员已经学会了如何使用命令式(过程式或面向对象)范式进行编程,因此可变性是其定义计算的基本方法,也就是说,他们没有因为不熟悉不变性,所以知道何时以及如何使用不变性。
许多程序员过早担心性能,而首先专注于编写功能上正确的程序然后尝试查找瓶颈通常更为有效。并对其进行优化。


评论


问题不只是速度。有许多有用的构造类型,如果不使用可变类型就无法实现。其中,两个线程之间的通信至少要求两个线程都必须引用一个共享对象,一个对象可以放置数据,而另一个对象可以读取它。此外,说“更改此对象的属性P和Q”比说“获取该对象,构造一个与之相似的新对象(除了P的值,然后再构造一个新对象)”在语义上要清晰得多。就像Q一样。”

–超级猫
2012年8月22日在22:55

我不是FP专家,但是AFAIK(1)线程通信可以通过在一个线程中创建一个不变值并在另一个线程中读取它来实现(同样,AFAIK,这是Erlang方法)(2)AFAIK设置属性的表示法不会有太大变化(例如,在Haskell中,setProperty记录值对应于Java record.setProperty(value)),只有语义会发生变化,因为设置属性的结果是一个新的不可变记录。

–乔治
2012年8月22日23:07



“两者都必须引用一个共享对象,一个对象可以放置数据,而另一个对象可以读取它”:更准确地说,您可以使对象不可变(所有成员都在C ++中为const或在Java中为final),并在构造函数。然后,将这个不变的对象从生产者线程移交给消费者线程。

–乔治
2012年8月22日23:12

(2)我已经将性能列为不使用不可变状态的原因。关于(1),实现线程之间通信的机制当然需要一些可变状态,但是您可以将其隐藏在编程模型中,例如演员模型(en.wikipedia.org/wiki/Actor_model)。因此,即使在较低的级别上需要可变性来实现通信,也可以在较高的抽象级别上进行来回发送不变对象的线程之间的通信。

–乔治
2012年8月22日23:21

我不确定我是否完全理解您,但是即使是每个值都不可变的纯函数式语言,也有一个可变的东西:程序状态,即当前程序堆栈和变量绑定。但这是执行环境。无论如何,我建议我们在聊天中讨论一些时间,然后从该问题中清除消息。

–乔治
2012年8月23日在16:04

#19 楼

我知道您在问Java,但是我在Objective-C中始终使用可变与不可变。有一个不可变数组NSArray和一个可变数组NSMutableArray。这是两个不同的类,它们以优化的方式专门编写以处理确切的用法。如果我需要创建一个数组而从不更改其内容,则可以使用NSArray,它是一个较小的对象,与可变数组相比,它的运行速度要快得多。

因此,如果您创建一个不可变的Person对象,那么您只需要一个构造函数和getters,因此该对象将更小并且使用更少的内存,从而使您的程序实际上更快。如果您需要在创建后更改对象,那么可变的Person对象会更好,这样它可以更改值而不是创建新对象。

因此:根据您打算使用的对象对象,在性能方面,选择可变与不可变可以产生巨大的差异。

#20 楼

除了这里给出的许多其他原因之外,一个问题是主流语言不能很好地支持不变性。至少,您因不变性而受到惩罚,因为您必须添加其他关键字(例如const或final),并且必须使用具有许多参数或代码冗长的构建器模式的难以理解的构造器。

考虑到不变性的语言,这要容易得多。考虑以下Scala代码片段,该代码片段用于定义类Person(可以选择使用命名参数)并创建具有更改的属性的副本:

case class Person(id: String, firstName: String, lastName: String)

val joe = Person("123", "Joe", "Doe")
val spouse = Person(id = "124", firstName = "Mary", lastName = "Moe")
val joeMarried = joe.copy(lastName = "Doe-Moe")


因此,如果您希望开发人员采用不变性,这就是您可能会考虑改用更现代的编程语言的原因之一。

评论


在之前的24个答案中所提出和解释的观点看来,这似乎并没有增加任何实质性内容

– gna
2014年12月9日上午8:13

@gnat前面哪个答案表明大多数主流语言都没有适当地支持不变性?我认为这一点根本没有提出(我检查过),但这是恕我直言,这是一个非常重要的障碍。

–汉斯·彼得·斯托尔
2014-12-09 20:10



例如,这一节深入解释了Java中的这一问题。至少有3个其他答案与之间接相关

– gna
2014-12-09 20:28



#21 楼

不可变意味着您不能更改值,而可变意味着如果您考虑基元和对象,则可以更改值。
对象与Java中的基元不同,因为它们是内置类型的。基元以int,boolean和void等类型构建。

许多人认为在其前面具有最终修饰符的基元和对象变量是不可变的,但实际上并非如此。所以final几乎并不意味着变量是不变的。查看此链接以获取代码示例:

public abstract class FinalBase {

    private final int variable; // Unset

    /* if final really means immutable than
     * I shouldn't be able to set the variable
     * but I can.
     */
    public FinalBase(int variable) { 
        this.variable = variable;
    }

    public int getVariable() {
        return variable;
    }

    public abstract void method();
}

// This is not fully necessary for this example
// but helps you see how to set the final value 
// in a sub class.
public class FinalSubclass extends FinalBase {

    public FinalSubclass(int variable) {
        super(variable);
    }

    @Override
    public void method() {
        System.out.println( getVariable() );
    }

    @Override
    public int getVariable() {

        return super.getVariable();
    }

    public static void main(String[] args) {
        FinalSubclass subclass = new FinalSubclass(10);
        subclass.method();
    }
}


#22 楼

我认为,一个很好的理由将是对“真实的”可变“对象”进行建模,例如界面窗口。我隐约记得曾经读过OOP是在有人尝试编写软件来控制某些货运港口操作时发明的。

评论


我还没有找到关于OOP起源的文章,但是据Wikipedia所说,大型集装箱运输公司OOCL的某些综合区域信息系统是用Smalltalk编写的。

– Alexey
2014年1月1日9:55



#23 楼

Java在许多情况下都要求使用可变对象,例如,当您想要对访问者或可运行的对象进行计数或返回时,即使没有多线程处理,也需要最终变量。

评论


最终实际上大约是迈向不变的1/4。不知道为什么你提到它。

– cHao
2012年6月7日下午4:11

你真的看过我的帖子吗?

– Ralf H
2012年12月23日在18:16

你真的读过这个问题吗? :)它与final完全无关。在这种情况下提出这个建议,就会使final与“ mutable”真正混为一谈,而final的重点是防止某些类型的变异。 (顺便说一句,不是我的不赞成。)

– cHao
2012年12月23日在19:45

我并不是说您在这里的某个地方没有正确的观点。我是说您的解释不是很好。我稍微看一下您可能会在哪里使用它,但是您需要走得更远。照原样,它看起来很混乱。

– cHao
2012-12-23 19:59



实际上,很少有需要使用可变对象的情况。在某些情况下,使用可变对象可以使代码更清晰,或在某些情况下更快。但是请考虑到,绝大多数设计模式基本上只是函数式编程的半成品,而这些函数式编程并不支持本机语言。有趣的是,FP仅要求在非常选定的位置(即必须发生副作用的位置)进行可变性,并且这些位置通常在数量,大小和范围上受到严格限制。

– cHao
2012-12-27 15:32