我已经阅读了几篇关于不变性的文章,但仍然没有很好地遵循这个概念.
我最近在这里提到了一个线程,提到了不变性,但由于这本身就是一个话题,我现在正在制作一个专门的线程.
我在过去的帖子中提到过,我认为不变性是将对象设为只读并使其可见性低的过程.另一位成员表示,这与此没有任何关系.此页面(系列的一部分)使用不可变类/结构的示例,它使用readonly和其他概念将其锁定.
在这个例子中,状态的定义究竟是什么?国家是一个我没有真正掌握的概念.
从设计指南的角度来看,一个不可变的类必须是一个不接受用户输入并且真的只返回值的类?
我的理解是,任何只返回信息的对象都应该是不可变的并且"锁定",对吧?因此,如果我想在具有该方法的专用类中返回当前时间,我应该使用引用类型,因为它将工作类型的引用,因此我受益于不变性.
不变性主要应用于对象(字符串,数组,自定义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是不可变的,你就可以保证这个事实.因此,不可变对象可以帮助实现良好的面向对象编程实践.
使不可变对象线程安全更容易
它可能简化了类的实现(如果你是编写类的人,那就太好了)
如果你要获取所有对象的实例变量并在纸上写下它们的值,那就是该特定时刻该对象的状态.程序的状态是给定时刻的所有对象的状态.国家随时间迅速变化; 程序需要改变状态才能继续运行.
然而,不可变对象随时间具有固定状态.一旦创建,虽然整个程序的状态可能,但不可变对象的状态不会改变.这样可以更容易地跟踪正在发生的事情(并查看上面的其他好处).
简单地说,内存在初始化后未被修改时是不可变的.
用C,Java和C#等命令式语言编写的程序可以随意操作内存数据.物理存储器区域一旦被搁置,可以在程序执行期间的任何时间由执行线程全部或部分地修改.事实上,命令式语言鼓励这种编程方式.
以这种方式编写程序对于单线程应用程序来说非常成功.然而,随着现代应用程序开发在单个进程内向多个并发操作线程移动,引入了潜在问题和复杂性的世界.
当只有一个执行线程时,你可以想象这个单线程"拥有"内存中的所有数据,因此可以随意操作它.但是,当涉及多个执行线程时,没有隐含的所有权概念.
相反,这种负担落在程序员身上,他们必须竭尽全力确保内存结构对所有读者都处于一致状态.必须谨慎使用锁定结构,以防止一个线程在被另一个线程更新时看到数据.如果没有这种协调,线程将不可避免地消耗仅在更新中途的数据.这种情况的结果是不可预测的,而且往往是灾难性的.此外,在代码中正确地进行锁定是非常困难的,并且如果做得不好可能会削弱性能,或者在最坏的情况下,会导致执行无法恢复的情况死锁.
使用不可变数据结构减少了将复杂锁定引入代码的需要.当一段存储器保证在程序的生命周期内不会改变时,多个读取器可以同时访问存储器.他们不可能在不一致的状态下观察特定数据.
许多函数式编程语言,如Lisp,Haskell,Erlang,F#和Clojure,本质上鼓励不可变数据结构.正因为这个原因,随着我们逐渐走向越来越复杂的多线程应用程序开发和多计算机计算机体系结构,它们正在重新兴起.
州应用程序的状态可以简单地被视为在给定时间点的所有存储器和CPU寄存器的内容.
从逻辑上讲,程序的状态可以分为两个:
堆的状态
每个执行线程的堆栈状态
在诸如C#和Java的托管环境中,一个线程无法访问另一个线程的内存.因此,每个线程'拥有'其堆栈的状态.可以将堆栈视为保存值type(struct
)的局部变量和参数,以及对象的引用.这些值与外部线程隔离.
但是,堆上的数据可在所有线程之间共享,因此必须小心控制并发访问.所有reference-type(class
)对象实例都存储在堆上.
在OOP中,类的实例的状态由其字段确定.这些字段存储在堆上,因此可以从所有线程访问.如果一个类定义了允许在构造函数完成后修改字段的方法,那么该类是可变的(不是不可变的).如果无法以任何方式更改字段,则类型是不可变的.值得注意的是,具有所有C#readonly
/ Java final
字段的类不一定是不可变的.这些构造确保引用不能更改,但不能更改引用的对象.例如,字段可以具有对对象列表的不可改变的引用,但是可以在任何时间修改列表的实际内容.
通过将类型定义为真正不可变的类型,可以将其状态视为已冻结,因此该类型对于多个线程的访问是安全的.
实际上,将所有类型定义为不可变类可能不方便.修改不可变类型的值可能涉及相当多的内存复制.有些语言使这个过程比其他语言更容易,但无论哪种方式,CPU最终都会做一些额外的工作.许多因素有助于确定复制内存所花费的时间是否超过锁定争用的影响.
许多研究已经用于开发不可变数据结构,例如列表和树.当使用这样的结构时,例如列表,"添加"操作将返回对添加了新项目的新列表的引用.对前一个列表的引用看不到任何更改,仍然具有一致的数据视图.
简单来说:一旦创建了不可变对象,就无法更改该对象的内容..Net不可变对象的示例是String和Uri.
修改字符串时,只需获取一个新字符串即可.原始字符串不会更改.Uri只有只读属性,没有可用于更改Uri内容的方法.
不可变对象很重要的案例是多种多样的,在大多数情况下都与安全性有关.Uri就是一个很好的例子.(例如,您不希望某些不受信任的代码更改Uri.)这意味着您可以将引用传递给不可变对象,而不必担心内容会发生变化.
希望这可以帮助.
永不改变的事情永远不会改变.可变的东西可以改变.可变的东西变异.不可改变的事物似乎发生了变化,但实际上创造了一个新的可变事物
例如,这是Clojure中的地图
(def imap {1 "1" 2 "2"}) (conj imap [3 "3"]) (println imap)
第一行创建一个新的不可变Clojure映射.第二行将3和"3"连接到地图.这可能看起来好像在修改旧地图,但实际上它正在返回添加了3"3" 的新地图.这是不变性的一个主要例子.如果这是一个可变的地图,它只需将3"3"直接添加到同一个旧地图上.第三行打印地图
{3 "3", 1 "1", 2 "2"}
不可变性有助于保持代码清洁和安全.这个和其他原因是函数式编程语言倾向于倾向于不变性和较少有状态的原因.