如果不可变的对象很好,那么人们为什么继续创建可变的对象?[关闭]


250

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

我有4年的Java编程经验,正如我所看到的,创建类之后,人们要做的第一件事就是在IDE中生成getter和setter(因此使其可变)。是否缺乏意识,或者在大多数情况下我们能否摆脱使用可变对象的困扰?


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


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

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

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

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

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

Answers:


326

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

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

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

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

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


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

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

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

4
@KonradRudolph,要点,谢谢。我并不是要排除在复杂对象中使用不变性,而是要正确而有效地实现这样的类并不是一件容易的事,而且所需的额外工作可能并不总是合理的。
彼得Török

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

130

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


9
在我所知的大多数现代IDE中,仅生成吸气剂与生成吸气剂和设置器都需要花费几乎相同的精力。虽然这是事实,增加finalconst等需要一点额外的努力......除非你设置了代码模板:-)
彼得Török

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

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

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

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

49
  1. 有一个可变性的地方。 领域驱动的设计原则对什么应该是可变的和什么是不可变的提供了扎实的理解。如果您考虑一下,您将意识到构想一个系统,在该系统中,对对象的每次状态更改都需要销毁该对象以及对引用它的每个对象进行重新组合。对于复杂的系统,这很容易导致完全擦除和重建整个系统的对象图

  2. 大多数开发人员不会在性能要求足够重要以至于需要专注于并发性(或许多其他问题,而知情人士普遍认为是好的做法)方面做不到的事情。

  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;
          } 
        }
    

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

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

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

4
@AndresF。,因此,根据您的意思,如果Ris Relationship(a,b)和the abare都是不可变的,则ab不会保留对的引用R。为了使此工作正常,必须将引用存储在其他位置(例如静态类)。我是否正确理解您的意图?
smartcaveman 2012年

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

36

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

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

要意识到的另一件事是,如果您的代码没有以暴露并发性问题的方式运行,那么您可能需要花费多年的时间编写面向对象的代码,而从未真正意识到共享可变状态的可怕性。


3
这是冈崎的书吗?
机械蜗牛

是的 有点干但是有很多很好的信息……
Paul Sanwald 2012年

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

虽然最后一句在过去可能是真实的它不是明确表示,将在未来,鉴于目前的硬件发展趋势保持真实等等
JK。

29

在我看来,这是缺乏认识的。如果您查看其他已知的JVM语言(Scala,Clojure),则在代码中很少看到可变对象,这就是为什么人们在单线程不够的情况下开始使用它们的原因。

我目前正在学习Clojure,并且在Scala方面有一点经验(也有4年以上Java经验),并且由于对状态的了解,您的编码风格也发生了变化。


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

是的,这是正确的。
Florian Salihovic 2012年

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

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

13

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


4
+1,在第一次数据分析之后,将getter / setter模式作为某种默认实现经常使用。
Jaap 2012年

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

12

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

显然,可变对象比Hibernate出现的时间长得多,因此Hibernate可能不是可变对象流行的最初“原因”。也许可变对象的流行使Hibernate蓬勃发展。

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


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

9

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

许多程序旨在对固有可变的现实事物进行建模。假设在12:51 am,有一个变量AllTrucks持有对对象#451的引用,该对象是数据结构的根,该数据结构指示当时(12:51 am)车队的所有卡车中都装有什么货物,还有一个变量BobsTruck可以用来获取对#24601对象的引用,该对象指向一个对象,该对象指示当时(12:51 am)Bob的卡车中所装的货物。在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的卡车,然后简单地每当它想查看鲍勃卡车的当前状态时,都可以访问该对象。

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

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


8

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

我认为回答您问题的最佳和最快方法是让您掌握“不变性与可变性”的优缺点


7

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

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

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


5

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

与此相关的四个问题主要与数据绑定有关

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

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


5

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

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

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

我还发现,大多数程序员在他们的包装盒中都很高兴,并且常常抵制他们不完全理解的新想法。一般来说,人们不喜欢改变。

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


4

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


4

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

因此,建立在不可变对象之上的任何编程语言在其实现中都存在表示空白。对于像C这样的早期语言,这是一个很大的绊脚石。


1

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

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

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

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

作为PS,我感觉到Java获取器和设置器已失去控制。检查这个出来。


7
不可变状态仍然是状态。
Jeremy Heiler 2012年

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

1

很多人都给出了很好的答案,所以我想指出您所提及的内容,这些内容非常引人注目,而且非常真实,在这里没有其他地方提及。

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

实际上,尽管您非常需要定期使用getter,但是setter或可写属性应该在代码中永远存在的唯一方式是通过构建器模式,在完全实例化对象之后将其锁定。

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

现在,这也不是真正适用于“脚本”样式的代码/语言,而是适用于您为其他人创建的代码,并期望其他人多年来可以反复阅读。我最近不得不开始做出这种区分,因为我非常喜欢Groovy,并且目标也有很大的不同。


1

实例化对象后,如果必须设置倍数,则使用可变对象。

您不应有一个带有六个参数的构造函数。而是使用setter方法修改对象。

一个示例是一个Report对象,带有用于字体,方向等的设置器。

简而言之:当您有很多状态要设置给对象,并且很长的构造函数签名不可行时,可变变量会很有用。

编辑:生成器模式可用于生成对象的整个状态。


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

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

1
@cHao如果要设置10或15个属性怎么办?同样,名为的方法withFont返回似乎也不好Report
图兰斯·科尔多瓦

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

1
@cHao为20个属性建立调用链是很丑的。丑陋的代码往往是劣质代码。
图兰斯·科尔多瓦

1

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

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

功能方法可以更简洁,更健壮,但是由于复制,它的效率可能非常低,因此您需要使用逐步修改的共享数据结构。

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

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

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

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

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

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

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

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

1

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

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

因此:根据性能的不同,选择可变对象还是不可变对象可能会产生很大的不同。


1

除了这里给出的许多其他原因之外,一个问题是主流语言不能很好地支持不变性。至少,您因不变性而受到惩罚,因为您必须添加其他关键字(例如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个答案中提出和解释的要点增加任何实质性的内容
gnat 2014年

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

这其中例如去深入解释Java的这个问题。至少还有3个其他与答案间接相关的答案
gnat 2014年

0

不可变意味着您不能更改值,而可变意味着如果您考虑原语和对象,则可以更改值。对象与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();
    }
}

-1

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


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

-2

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


5
final实际上大约是迈向不变的1/4 。没有看到你为什么要提到它。
cHao 2012年

你真的看过我的帖子吗?
拉尔夫·H

你真的读过这个问题吗?:)它与绝对无关final。把它在这方面想起了一个非常奇怪的归并final与“可变”,当的很点final是为了防止某些类型的突变。(顺便说一句,不是我的
不赞成

我并不是说您在这里某处没有正确的观点。我是说您的解释不是很好。我有点半看你可能会去哪里,但你需要走的更远。照原样,它看起来很混乱。
cHao 2012年

1
实际上,很少有需要可变对象的情况。在某些情况下,使用可变对象会使代码更清晰,或在某些情况下更快。但是请考虑到,绝大多数设计模式基本上只是函数式编程的半成品,而这些函数式编程并不支持本机语言。有趣的是,FP仅需要在非常选定的位置(即必须发生副作用的位置)进行可变性,并且这些位置通常在数量,大小和范围上受到严格限制。
cHao 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.