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

时间:2009-03-07 22:50:06

标签: oop immutability

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

我最近在这里提到了一个线程,它提到了不变性,但由于这本身就是一个话题,我现在正在制作一个专门的线程。

我在过去的帖子中提到,我认为不变性是将对象设为只读并使其可见性低的过程。另一位成员表示,这与此没有任何关系。 This pageseries的一部分)使用不可变类/结构的示例,它使用readonly和其他概念将其锁定。

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

从设计指南的角度来看,一个不可变的类必须是一个不接受用户输入并且真的只返回值的类?

我的理解是,任何只返回信息的对象都应该是不可变的并且“锁定”,对吧?因此,如果我想在具有该方法的专用类中返回当前时间,我应该使用引用类型,因为它将使用该类型的引用,因此我从不变性中受益。

15 个答案:

答案 0 :(得分:57)

什么是不变性?

  • 不变性主要应用于对象(字符串,数组,自定义Animal类)
  • 通常,如果存在类的不可变版本,则还提供可变版本。例如,Objective-C和Cocoa定义了NSString类(不可变)和NSMutableString类。
  • 如果对象是不可变的,则在创建后不能更改它(基本上是只读的)。您可以将其视为“只有构造函数才能更改对象”。

这与用户输入没有任何关系;甚至你的代码都不能改变不可变对象的值。但是,您始终可以创建一个新的不可变对象来替换它。这是一个伪代码示例;请注意,在许多语言中,您只需执行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

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

不可变性的好处

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

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

    如果lowercase()的实现改变了myString,因为它创建了一个小写版本,该怎么办?第三行不会给你想要的结果。当然,一个好的lowercase()函数不会这样做,但如果myString是不可变的,你就可以保证这个事实。因此,不可变对象可以帮助实现良好的面向对象编程实践。

  • 使不可变对象线程安全更容易

  • 它可能简化了类的实现(如果你是编写类的人,那就太好了)

国家

如果您要获取所有对象的实例变量并在纸上记下它们的值,那将是该特定时刻该对象的状态。程序的状态是给定时刻的所有对象的状态。国家随时间迅速变化;程序需要改变状态才能继续运行。

然而,不可变对象随着时间的推移具有固定状态。一旦创建,虽然整个程序的状态可能,但不可变对象的状态不会改变。这样可以更容易地跟踪正在发生的事情(并看到上面的其他好处)。

答案 1 :(得分:20)

不变性

简单地说,内存在初始化后未被修改时是不可变的。

用C,Java和C#等命令式语言编写的程序可以随意操作内存中的数据。物理存储器区域一旦被搁置,可以在程序执行期间的任何时间由执行线程全部或部分地修改。事实上,命令式语言鼓励这种编程方式。

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

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

相反,这种负担落在程序员身上,他们必须竭尽全力确保内存结构对所有读者都处于一致状态。必须谨慎使用锁定结构,以防止一个线程在被另一个线程更新时看到数据。如果没有这种协调,线程将不可避免地消耗仅在更新中途的数据。这种情况的结果是不可预测的,而且往往是灾难性的。此外,在代码中正确地进行锁定是非常困难的,如果做得不好可能会削弱性能,或者在最坏的情况下,会导致无法恢复执行的情况死锁。

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

许多函数式编程语言,如Lisp,Haskell,Erlang,F#和Clojure,本质上鼓励不可变数据结构。正是出于这个原因,随着我们逐渐走向越来越复杂的多线程应用程序开发和多计算机计算机体系结构,它们正在重新兴起。

国家

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

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

  1. 堆的状态
  2. 每个执行线程的堆栈状态
  3. 在C#和Java等托管环境中,一个线程无法访问另一个线程的内存。因此,每个线程'拥有'其堆栈的状态。可以将堆栈视为保存值类型(struct)的局部变量和参数,以及对对象的引用。这些值与外部线程隔离。

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

    在OOP中,类的实例的状态由其字段确定。这些字段存储在堆上,因此可以从所有线程访问。如果一个类定义了允许在构造函数完成后修改字段的方法,那么该类是可变的(不是不可变的)。如果无法以任何方式更改字段,则类型是不可变的。值得注意的是,具有所有C#readonly / Java final字段的类不一定是不可变的。这些构造确保引用不能更改,但不能更改引用的对象。例如,字段可能具有对象列表的不可更改引用,但可以随时修改列表的实际内容。

    通过将类型定义为真正不可变,可以将其状态视为已冻结,因此该类型对于多个线程的访问是安全的。

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

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

答案 2 :(得分:8)

简单地说:创建不可变对象后,无法更改该对象的内容。 .Net不可变对象的示例是String和Uri。

修改字符串时,只需获取一个新字符串即可。原始字符串不会更改。 Uri只有只读属性,没有可用于更改Uri内容的方法。

不可变对象很重要的案例是多种多样的,并且在大多数情况下都与安全性有关。 Uri就是一个很好的例子。 (例如,您不希望某些不受信任的代码更改Uri。)这意味着您可以将引用传递给不可变对象,而不必担心内容会发生变化。

希望这会有所帮助。

答案 3 :(得分:6)

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

例如,这是Clojure中的地图

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

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

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

不变性有助于保持代码清洁和安全。这个和其他原因是函数式编程语言倾向于倾向于不变性和较少有状态的原因。

答案 4 :(得分:3)

好问题。

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

显然,如果没有可变性,除非保存复杂的计算,否则无法实现这一目标,因此您通常需要一些可变性来创建功能性商业软件。然而,值得认识到不变性应该在哪里,例如任何交易。

查看函数式编程和纯度概念,了解有关该哲学的更多信息。你在调用堆栈上存储的越多(你传递给方法的参数),而不是通过诸如集合或静态可用对象之类的引用使它们可用,你的程序就越纯粹,你就越不容易出现竞争条件。如今有了更多的多核,这个话题就更为重要了。

此外,不变性还可以减少程序中的可能性,从而降低潜在的复杂性和漏洞的可能性。

答案 5 :(得分:2)

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

不变性通常也意味着您可以将对象视为“值”,并且对象的相同副本与对象本身之间没有有效差异。

答案 6 :(得分:2)

让我再补充一点。除了上面提到的所有内容之外,您还需要不可变性:

答案 7 :(得分:1)

使事物不可变可以防止大量常见错误。

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

如果事情是可变的,并且当你传递它们时你不希望它们改变,你需要制作一个你传递的防御副本。然后,如果您调用的方法/函数更改了项目的副本,则原始文件不会受到影响。

使事物不可变意味着你不必记住(或花时间/记忆)制作防御副本。

如果你真的对它起作用,并考虑你所做的每个变量,你会发现绝大多数(我通常有90-95%)你的变量一旦给出一个值就不会改变。这样做可以使程序更容易理解并减少错误数量。

要回答关于状态的问题,state是“对象”(是类或结构)的变量所具有的值。如果你拿一个人“对象”的状态会是眼睛颜色,头发颜色,头发长度等...其中一些(说眼睛的颜色)不会改变,而其他人,如头发长度确实改变。

答案 8 :(得分:1)

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

一个实际的例子是字符串的重复串联。例如在.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非常重要。

答案 9 :(得分:1)

您无法更改不可变对象,因此必须更换它......“改变它”。即替换然后丢弃。在这个意义上“替换”意味着将指针从一个存储位置(旧值)更改为另一个(对于新值)。

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

string mystring = "inital value";
mystring = "new value";
System.Console.WriteLine(mystring); // Outputs "new value";
并且自己想,“但是我正在改变它,看起来就在那里,黑白分明!mystring输出'新价值'......我以为你说我无法改变它?!!”

但实际上在幕后,最新发生的是新内存的分配,即mystring现在指向不同的内存地址和空间。在这个意义上,“不可变”不是指mystring的值,而是指变量mystring用来存储其值的内存。

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

其中一个真正爆发的地方:内存使用是在高度迭代的循环中,特别是在Ashs的帖子中使用字符串。假设您在迭代循环中构建一个HTML页面,在那里您不断地将下一个HTML块附加到最后一个,并且只是为了踢,您在高容量服务器上执行此操作。这种“新价值记忆”的不断分配很快就会变得昂贵,并且如果“旧的价值记忆”没有得到适当的清理,最终会致命。

另一个问题是有些人认为像垃圾收集(GC)这样的事情会立即发生。但事实并非如此。存在各种优化,使得垃圾收集被设置为在更多空闲时段期间发生。因此,在将内存标记为已丢弃和实际由垃圾收集器释放之间可能存在显着延迟....因此,如果您只是将问题推迟到GC,则可能会遭受大量内存使用高峰。

如果GC在内存不足之前没有机会运行,那么事情就不会像其他没有自动垃圾收集的语言那样坍塌。相反,GC将作为最高优先级流程启动,以释放丢弃的内存,无论时机有多糟糕,并在清理时成为阻塞进程。显然,这并不酷。

基本上,您需要记住这些内容的代码,并查看您正在使用的语言的文档,以获得最佳实践/模式,以避免/降低此风险。

正如在Ashs的帖子中,在.Net和字符串中,建议的做法是使用可变的StringBuilder类,而不是在不断更改字符串值的需要时使用不可变的字符串类。

其他语言/类型同样有自己的解决方法。

答案 10 :(得分:1)

为什么不变性?

  1. 他们不容易出错并且更安全。

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

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

  4. 不可变对象是良好的Map键和Set元素,因为一旦创建它们通常不会改变。

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

  6. 不可变性使得程序并行化变得更容易,因为对象之间没有冲突。

  7. 即使您有例外情况,程序的内部状态也会保持一致。

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

  9. 请参阅我的博客以获得更详细的答案:
    http://javaexplorer03.blogspot.in/2015/07/minimize-mutability.html

答案 11 :(得分:0)

看,我还没看过您发布的链接。

但是,这是我的理解 每个程序都掌握一些数据(状态)的知识,可以通过用户输入/外部更改等来改变。

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

AFAIK,函数式编程有不可变的东西(即你不能将赋值用于保存值的变量。你可以做的是创建另一个可以保存原始值+变量的变量)。

.net有一个字符串类,这是一个例子 即你不能在其位置修改字符串

string s =“hello”; 我可以写s.Replace(“el”,“a”);但这不会修改变量s的内容。

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

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

编辑:不可变=一旦它保持一些价值而无法分配&amp;不能替换到位(也许?)

答案 12 :(得分:0)

WPF API中提供了不可变对象提供的潜在性能优势的示例。许多WPF类型的公共基类是Freezable

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

我个人希望用我最常用的语言C#来表达不变性的概念。有一个readonly修饰符可用于字段。我希望在类型上看到一个readonly修饰符,只允许那些只具有只读类型的只读字段的类型。基本上这意味着所有状态都需要在构造时注入,并且整个对象图将被冻结。我想这是CLR固有的元数据,然后它可以很容易地用于优化GC的垃圾分析。

答案 13 :(得分:0)

抱歉,为什么不变性会阻止竞争条件(在本例中,在读取危险后写入)?

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

答案 14 :(得分:0)

不变性是关于价值的,价值是关于事实的。如果不可更改,某些东西就有价值,因为如果某些东西可以改变,那么这意味着没有特定的值可以连接到它。对象用状态A初始化,并且在程序执行期间变为状态B和状态C.这意味着对象不代表单个特定值,而只是一个容器,对内存中某个位置的抽象,仅此而已。你无法信任这样的容器,你不能相信这个容器有你想要的价值。

让我们举例 - 让我们想象一下,在代码中创建了Book类的实例。

Book bookPotter =  new Book();
bookPotter.setAuthor('J.K Rowling');
bookPotter.setTitle('Harry Potter');

此实例的某些字段设置为author和title。一切都很好,但在代码的某些部分再次使用了setter。

Book bookLor =  bookPotter; // only reference pass
bookLor.setAuthor('J.R.R Tolkien');
bookLor.setTitle('Lords of The Rings');

不要被不同的变量名称欺骗,实际上它是同一个实例。代码再次在同一个实例上使用setter。这意味着bookPotter从来就不是哈利波特的书,bookPotter只是指向未知书所在地的指针。也就是说,看起来它更像书架上的书架。你对这样的对象有什么信任?这是哈利波特的书或LoR书还是两本书?

类的可变实例只是指向具有类特征的未知状态的指针。

然后如何避免变异?规则很容易:

  • 通过构造函数或构建器构建具有所需状态的对象
  • 不为对象的封装状态创建setter
  • 不要在其任何方法中更改对象的任何封装状态

这些规则将允许更可预测和更可靠的对象。回到我们的示例并按照上述规则进行预订:

Book bookPotter =  new Book('J.K Rowling', 'Harry Potter');
Book bookLor = new Book('J.R.R Tolkien', 'Lord of The Rings');

一切都是在构建阶段设置的,在本例中是构造函数,但对于更大的结构,它可以是构建器。对象中不存在setter,book不能变异为不同的。在这种情况下,bookPotter代表了哈利波特书的价值,你可以肯定这是不可改变的事实。

如果您对更广泛的不变性感兴趣,那么在这篇中等文章中更多地讨论与JavaScript相关的主题 - https://medium.com/@macsikora/the-state-of-immutability-169d2cd11310