首页 文章

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

提问于
浏览
56

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

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

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

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

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

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

15 回答

  • 1

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

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

    什么是不变性?

    • Immutability主要应用于对象(字符串,数组,自定义Animal类)

    • 通常,如果存在类的不可变版本,则还提供可变版本 . 例如,Objective-C和Cocoa定义了NSString类(不可变)和NSMutableString类 .

    • 如果对象是不可变的,则在创建对象后无法更改(基本上只读) . 你可以把它想象成"only the constructor can change the object" .

    这不是一个伪代码示例 . 请注意,在许多语言中,您可以简单地执行 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,因为它创建了一个小写版本,该怎么办?第三行不会给你想要的结果 . 当然,如果myString是不可变的,那么一个好的 lowercase() 函数就不会保证这个事实 . 因此,不可变对象可以帮助实现良好的面向对象编程实践 .

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

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

    国家

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

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

  • 3

    不变性

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

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

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

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

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

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

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

    国家

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

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

    • 堆的状态

    • 每个执行线程的堆栈状态

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

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

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

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

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

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

  • 1

    简单来说:一旦创建了不可变对象,就无法更改该对象的内容 . .Net不可变对象的示例是String和Uri .

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

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

    希望这可以帮助 .

  • 1

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

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

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

  • 6

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

    例如,这是Clojure中的 Map

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

    第一行创建一个新的不可变Clojure映射 . 第二行将3和"3"连接到 Map . 这可能看起来好像在修改旧 Map ,但实际上它正在返回添加了3 "3"的新 Map . 这是不变性的一个主要例子 . 如果这是一个可变的 Map ,它只会直接添加3 "3" to 同一个旧 Map . 第三行打印 Map

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

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

  • 0

    好问题 .

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

    显然,如果没有可变性,除了复杂的计算之外,你无法做到这一点,所以你通常需要一些可变性来创建功能性商业软件 . 然而,值得认识到不变性应该在哪里,例如任何交易 .

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

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

  • 20

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

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

  • 0

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

  • 8

    使事情不可改变可以防止大量常见错误 .

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

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

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

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

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

  • 0

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

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

  • 1

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

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

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

    并且想一想,“但是我正在改变它,看着那里,黑色和白色!神秘输出“新 Value ”......我以为你说我无法改变它?!!“

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

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

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

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

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

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

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

    其他语言/类型也会有类似的解决方法 .

  • 2

    为什么不变?

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

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

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

    • 不可变对象是良好的Map键和Set元素,因为这些通常在创建后不会更改 .

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

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

    • 即使您有例外,程序的内部状态也将保持一致 .

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

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

  • 56

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

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

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

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

    .net有字符串类,这是一个例子 .
    即您无法在其位置修改字符串

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

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

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

    编辑:不可变=一旦它持有一些值而无法分配并且无法替换到位(也许?)

  • 0

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

    让我们来举例 - 让我们想象在代码中创建Book类的实例 .

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

    此实例有一些字段设置,如作者和 Headers . 一切都很好,但在代码的某些部分再次使用了setter .

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

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

    Mutable instance of a class is only a pointer to an unknown state with the class characteristics.

    那怎么避免变异呢?规则很容易:

    • 通过构造函数或构建器构造具有所需状态的对象

    • 不为对象的封装状态创建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代表了哈利波特书的 Value ,你可以肯定这是不可改变的事实 .

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

相关问题