什么是不变性,我为什么要担心呢?

我读过几篇关于不变性的文章,但是仍然没有很好地遵循这个概念。

我最近在这里提到了一个提到不变性的话题,但是因为这本身就是一个话题,所以我现在正在制定一个专门的话题。

我在过去的主题中提到,我认为不变性是指只读取对象并使其可见性较低的过程。 另一位成员则表示,这并没有什么关系。 这个页面 ( 一系列的一部分)使用一个不可变的类/结构的例子,它使用只读和其他概念来locking它。

在这个例子中,状态的定义究竟是什么? 国家是一个我没有真正掌握的概念。

从devise原则的angular度来看,一个不可变的类必须是一个不接受用户input的类,真的只是返回值呢?

我的理解是任何只是返回信息的对象都应该是不可变的,“locking”的,对吗? 所以如果我想用一个方法返回一个专门的类的当前时间,我应该使用一个引用types,因为这将是一个types的引用,因此我受益于不变性。

什么是不可变性?

  • 不变性主要应用于对象(string,数组,自定义动物类)
  • 通常情况下,如果有一个类的不可变版本,一个可变版本也是可用的。 例如,Objective-C和Cocoa定义了一个NSString类(不可变的)和一个NSMutableString类。
  • 如果一个对象是不可变的,创build后就不能改变(基本上只读)。 你可以把它想成“只有构造函数才能改变对象”。

这不直接与用户input有关; 甚至连你的代码都不能改变一个不可变对象的值。 但是,您始终可以创build一个新的不可变对象来replace它。 这是一个伪代码示例; 请注意,在许多语言中,你可以简单地做myString = "hello"; 而不是像以下那样使用构造函数,但为了清楚起见,我将其包含在内:

 String myString = new ImmutableString("hello"); myString.appendString(" world"); // Can't do this myString.setValue("hello world"); // Can't do this myString = new ImmutableString("hello world"); // OK 

你提到“只是返回信息的对象”; 这不会自动使其成为不变性的良好候选者。 不可变的对象往往会返回与它们构build的值相同的值,所以我倾向于说现在的时间不会很理想,因为经常变化。 但是,您可以创build一个MomentOfTime类,该类使用特定的时间戳创build,并且将来总是返回一个时间戳。

Immutabilty的好处

  • 如果你将一个对象传递给另一个函数/方法,那么在函数返回之后,你不必担心该对象是否具有相同的值。 例如:

     String myString = "HeLLo WoRLd"; String lowercasedString = lowercase(myString); print myString + " was converted to " + lowercasedString; 

    如果lowercase()的实现更改myString,因为它正在创build一个小写的版本? 第三行不会给你你想要的结果。 当然,一个好的lowercase()函数不会这样做,但是如果myString是不可变的,则可以保证这一点。 因此,不可变的对象可以帮助执行良好的面向对象编程实践。

  • 使不可变对象更安全

  • 它可能会简化课程的实施(如果你是写课程的人,那很好)

如果你把所有对象的实例variables都写下来,并把它们的值写在纸上,这就是那个对象在给定时刻的状态。 程序的状态是给定时刻所有对象的状态。 状态随着时间的推移迅速变化 一个程序需要改变状态才能继续运行。

然而,不变的对象随着时间的推移已经固定了。 一旦创build,一个不可变对象的状态不会改变,尽pipe整个程序的状态可能是这样的。 这使得更容易跟踪正在发生的事情(并看到上面的其他好处)。

不变性

简而言之,内存在被初始化后没有被修改时是不可变的。

使用命令式语言(如C,Java和C#)编写的程序可以随意操作内存数据。 物理内存区域一旦被预留,可以在程序执行期间的任何时候通过执行的线程全部或部分修改。 事实上,命令式语言鼓励这种编程方式。

以这种方式编写程序对于单线程应用程序来说是非常成功的。 然而,随着现代应用程序开发在单个进程中向多个并行操作线程移动,引入了潜在问题和复杂性的世界。

当只有一个执行线程时,可以想象这个单线程拥有内存中的所有数据,因此可以随意操纵它。 但是,当涉及多个执行线程时,没有隐含的所有权概念。

相反,这种负担落在程序员身上,他们必须付出巨大的努力才能确保所有读者的内存结构处于一致的状态。 locking结构必须小心使用,以防止一个线程在被另一个线程更新时看到数据。 如果没有这种协调,一个线程将不可避免地消耗只是通过更新一半的数据。 这种情况的结果是不可预测的,往往是灾难性的。 而且,在代码中正确地进行locking是非常困难的,而且如果做得不好可能会削弱性能,或者在最坏的情况下会导致无法挽回的死锁。

使用不可变数据结构减轻了将复杂locking引入代码的需要。 当一段内存被保证不会在程序生命周期内改变时,多个阅读器可以同时访问内存。 他们不可能以不一致的状态观察特定的数据。

许多函数式编程语言(如Lisp,Haskell,Erlang,F#和Clojure)本质上鼓励不可变的数据结构。 正因为如此,当我们走向日益复杂的multithreading应用程序开发和多计算机计算机体系结构时,他们正在享受着兴趣的复苏。

应用程序的状态可以简单地认为是给定时间点所有内存和CPU寄存器的内容。

从逻辑上讲,一个程序的状态可以分为两个:

  1. 堆的状态
  2. 每个执行线程的堆栈状态

在C#和Java等托pipe环境中,一个线程无法访问另一个线程的内存。 因此,每个线程“拥有”它的堆栈状态。 栈可以被认为是保存局部variables和值types( struct )的参数,以及对象的引用。 这些值是从外部线程中分离出来的。

但是,堆中的数据可以在所有线程中共享,因此必须小心控制并发访问。 所有引用类( class )对象实例都存储在堆上。

在OOP中,类的一个实例的状态是由它的字段决定的。 这些字段存储在堆中,因此可以从所有线程访问。 如果一个类定义了允许在构造函数完成后修改字段的方法,那么这个类是可变的(不是不可变的)。 如果字段不能以任何方式改变,那么types是不可变的。 请注意,具有可变字段的类不一定是不可变的。 例如,在C#中,仅仅因为List<object>types的字段被定义为readonly ,列表的实际内容可能随时被修改。

通过将types定义为真正不可变,其状态可以被认为是冻结的,因此该types对于multithreading访问是安全的。

在实践中,将所有types定义为不可变是不方便的。 修改一个不可变types的值可能涉及到一点点的内存复制。 一些语言使得这个过程比其他语言更容易,但是无论哪种方式,CPU最终都会做一些额外的工作。 许多因素决定了复制内存所花费的时间是否超过了locking争用的影响。

很多研究已经进入了不可变的数据结构,如列表和树的开发。 当使用这样的结构时,比方说一个列表,“添加”操作将返回一个新列表的引用,添加新的项目。 对前一个列表的引用没有看到任何更改,并且仍然具有一致的数据视图。

简单来说:一旦你创build了一个不可变的对象,就没有办法改变这个对象的内容。 .Net不可变对象的例子是String和Uri。

当你修改一个string,你只需要一个新的string。 原来的string不会改变。 Uri只有只读属性,没有方法可以改变Uri的内容。

不可变对象很重要的情况是各种各样的,在大多数情况下都与安全有关。 Uri就是一个很好的例子。 (例如,你不希望Uri被一些不可信的代码改变)。这意味着你可以传递一个引用到一个不可变的对象,而不必担心内容将会改变。

希望这可以帮助。

不可改变的事情永远不会改变。 可变的事情可以改变。 可变的东西变异。 不变的东西似乎改变,但实际上创造了一个新的可变的东西。

例如这里是Clojure中的地图

 (def imap {1 "1" 2 "2"}) (conj imap [3 "3"]) (println imap) 

第一行创build一个新的不可变的Clojure地图。 第二行连接3和“3”到地图。 这可能看起来像是修改旧地图,但实际上它正在返回一个添加了3“3”的地图。 这是不变性的一个主要例子。 如果这是一个可变的地图,它会直接将3“3”直接添加相同的旧地图。 第三行打印地图

 {3 "3", 1 "1", 2 "2"} 

不变性有助于保持代码清洁和安全。 这和其他原因是为什么函数式编程语言往往倾向于不变性和不那么有状态。

好问题。

multithreading。 如果所有types都是不可变的,那么竞争条件就不存在了,你可以安全地在代码中抛出尽可能多的线程。

显然,如果没有可变性,你不可能完成这么多的工作,除非复杂的计算,所以你通常需要一些可变性来创buildfunction性的商业软件。 然而,值得认识的是不变性在哪里,比如任何交易。

查看函数式编程和纯度的概念以获取更多关于哲学的信息。 在调用堆栈上存储的越多(传递给方法的参数),而不是通过引用(如集合或静态可用对象)使其可用,则程序越纯粹,竞争条件越不容易。 随着更多的多核心这一天,这个话题更重要。

此外,不变性减less了程序的可能性,从而降低了潜在的复杂性和潜在的错误。

一个不可改变的对象是你可以安全地认为是不会改变的东西; 它有一个重要的属性,每个人都使用它可以假设他们看到相同的价值。

不变性通常也意味着你可以把对象看作是一个“价值”,并且对象的相同副本和对象本身之间没有有效的区别。

让我再补充一点。 除了上面提到的所有内容之外,你还希望不变:

  • 值对象(有时也称为数据对象或pojo)
  • 结构(在C#/。NET中) – 请参阅关于装箱的stackoverflow问题

让事物不可变防止大量的常见错误。

例如,一个学生不应该让他们的学生#改变他们。 如果您没有提供设置variables的方法(并使其成为const,或final,或者您的语言支持),那么您可以在编译时强制执行。

如果事情是可变的,而且当你传递它们时你不想让它们改变的话,你需要制作一份你通过的防御性的副本。 然后,如果您调用的方法/function改变了该项目的副本,则不改变原稿。

让事情变得不可变意味着你不必记住(或花时间/记忆)来制作保密的副本。

如果你真的在工作,想一想每个variables,你会发现绝大多数(通常有90-95%)的variables一旦被赋值,就不会改变。 这样做可以使程序更容易遵循并减less错误的数量。

为了回答你关于状态的问题,state是一个“对象”(是一个类或一个结构)的variables的值。 如果你把一个人的“物体”状态看成是眼睛的颜色,头发的颜色,头发的长度等等,那么其中的一些(比如说眼睛的颜色)就不会改变,而其他的如头发的长度也会改变。

“……我为什么要担心呢?”

一个实际的例子是string的重复连接。 在.NET中,例如:

 string SlowStringAppend(string [] files) { // Declare an string string result=""; for (int i=0;i<files.length;i++) { // result is a completely new string equal to itself plus the content of the new // file result = result + File.ReadAllText(files[i]); } return result; } string EfficientStringAppend(string [] files) { // Stringbuilder manages a internal data buffer that will only be expanded when absolutely necessary StringBuilder result=new SringBuilder(); for (int i=0;i<files.length;i++) { // The pre-allocated buffer (result) is appended to with the new string // and only expands when necessary. It doubles in size each expansion // so need for allocations become less common as it grows in size. result.Append(File.ReadAllText(files[i])); } return result.ToString(); } 

不幸的是,使用第一种(慢)函数方法仍然是常用的。 对不变性的理解使得使用StringBuilder非常重要。

你不能改变一个不可变的对象,所以你必须把它replace….“改变它”。 即replace然后丢弃。 在这个意义上,“replace”意味着将指针从一个内存位置(旧值)更改为另一个(新值)。

请注意,这样做,我们现在使用额外的内存。 一些旧的价值,一些新的价值。 另外请注意,有些人会因为看代码而感到困惑,比如:

 string mystring = "inital value"; mystring = "new value"; System.Console.WriteLine(mystring); // Outputs "new value"; 

自言自语,“但是我正在改变它,在那里看,在黑色和白色!mystring输出'新的价值'……我以为你说我不能改变它?!!”

但实际上,引发了这种新的内存分配,即mystring现在指向不同的内存地址和空间。 在这个意义上,“不可变”不是指mystring的值,而是指variablesmystring用来存储它的值的​​内存。

在某些语言中,存储旧值的内存必须手动清理,即程序员必须明确释放它,并记住这样做。 在其他语言中,这是该语言的自动特征,即.Net中的垃圾收集。

其中一个地方真的很stream行:内存的使用是在高度迭代的循环中,特别是在Ashs的post中。 假设你正在迭代循环中创build一个HTML页面,在这个循环中你不断地将下一个HTML块附加到最后,并且只是为了踢,你在高容量服务器上这样做。 如果“旧价值记忆”没有得到适当的清理,这种“新价值记忆”的持续分配可能很快变得昂贵,并最终致命。

另一个问题是有些人认为垃圾收集(GC)是立即发生的。 但事实并非如此。 存在各种优化,使垃圾收集被设置为在更多空闲期间发生。 因此,当内存被标记为废弃时,以及实际上被垃圾收集器释放时,可能会有显着的延迟。所以,如果您只是将问题推迟到GC,则可能会遇到大量内存使用率峰值。

如果GC在内存耗尽之前没有机会运行,那么事情就不会像其他没有自动垃圾收集的语言一样崩溃。 取而代之的是,GC将作为最高优先级的进程来释放丢弃的内存,无论时序如何糟糕,并在清理事件时成为阻塞进程。 显然,这不是很酷。

所以,基本上,您需要考虑这些事项,并查看您正在使用的语言的文档,以获得最佳实践/模式,从而避免/减轻此风险。

正如在Ashs的文章中,在.Net和string中,推荐的做法是使用可变的StringBuilder类,而不是不可变的string类,当需要不断更改string值时。

其他语言/types也会有类似的解决方法。

为什么不可变性?

  1. 他们不太容易出错,更安全。

  2. 不可变类比易变类更容易devise,实现和使用。

  3. 不可变的对象是线程安全的,所以没有同步问题。

  4. 不可变的对象是好的Map键和Set元素,因为这些元素一旦创build就不会改变。

  5. 不变性使编写,使用和推理代码变得更容易(类不变是一次build立,然后保持不变)。

  6. 由于不存在对象之间的冲突,不变性使并行化程序变得更容易。

  7. 即使你有例外,程序的内部状态也是一致的。

  8. 对不可变对象的引用可以被caching,因为它们不会改变(即在哈希中提供快速操作)。

看到我的博客更详细的答案:
http://javaexplorer03.blogspot.in/2015/07/minimize-mutability.html

看,我还没有读过你发布的链接。

不过,这是我的理解。
每个程序都有一些关于数据(状态)的知识,可以通过用户input/外部变化等来改变。

variables(变化的值)保持状态。 不可变意味着一些不会改变的数据。 你可以说,它是一样的只读或常量在某种程度上(可以这样看)。

AFAIK,函数式编程有一些不可变的东西(例如,你不能使用赋值给一个variables来保存值,你可以做的是创build另一个variables来保存原始值+变化)。

.net有一个例子的string类。
即你不能在其位置修改string

strings =“你好”; 我可以写s.Replace(“el”,“a”); 但是这不会修改variabless的内容。

我能做的是s = s.Replace(“el”,“a”);
这将创build一个新的variables&赋值给s(覆盖s的内容)。

如果我有我的理解,专家可以纠正错误。

编辑:不可变=一旦它拥有一些价值不可分配,不能在原地replace(也许?)

WPF API提供了不可变对象提供的潜在性能优势示例。 许多WPFtypes的通用基类是Freezable

几个WPF示例表明冻结对象(使它们在运行时不可变)可显着提高应用程序性能,因为不需要locking和复制。

就我个人而言,我希望不变性概念更容易用我最常用的语言C#来表示。 有一个readonly修饰符可用于字段。 我想在types上看到一个readonly修饰符,只允许只有readonlytypes为只读types的types。 基本上这意味着所有的状态将需要在施工时注入,而整个对象图将被冻结。 我想这是CLR固有的元数据,那么它可以很容易地用于优化GC的垃圾分析。

对不起,为什么不变性防止竞争条件(在这个例子中,读后危害)?

 shared v = Integer(3) v = Integer(v.value() + 1) # in parallel