为什么.NETstring是不可变的?

众所周知, String是不可变的。 String是不可变的, StringBuilder类是可变的吗?

  1. 不可变types的实例本质上是线程安全的,因为没有线程可以修改它,线程修改它干扰另一个的风险被删除(引用本身是另一回事)。
  2. 类似地,别名不能产生变化的事实(如果x和y都指向相同的对象,则对x的变化需要对y进行更改)允许相当程度的编译器优化。
  3. 节省内存的优化也是可能的。 实习和雾化是最明显的例子,虽然我们可以做同样的原则的其他版本。 我曾经通过比较不可变对象和replace对重复项的引用,以便它们都指向相同的实例(耗时,但是为了节省大量内存而增加启动时间)在这种情况下赢得胜利)。 使用可变对象无法完成。
  4. 除非将不可变types作为parameter passing给参数,否则不会产生副作用,除非它是outref (因为这会更改引用而不是对象)。 程序员因此知道,如果在方法的开始处string x = "abc" ,并且在方法的主体中没有改变,则在方法结束时x == "abc"
  5. 从概念上讲,语义更像是价值types; 特别是平等是基于国家而不是身份。 这意味着"abc" == "ab" + "c" 。 虽然这不需要不变性,但是对这样的string的引用在其整个生命周期中始终等于“abc”(这确实需要不变性),这使得用作关键字的地方在于保持与以前的值相等是重要的,更容易确保正确性(string确实常用作键)。
  6. 从概念上来说,做到一成不变是更有意义的。 如果我们在圣诞节增加一个月,我们还没有改变圣诞节,我们已经在一月下旬创造了一个新的date。 因此, Christmas.AddMonths(1)产生一个新的DateTime而不是改变一个可变的DateTime 。 (另一个例子,如果我作为一个可变的对象改变了我的名字,改变的是我使用的是哪个名字,“Jon”保持不变,其他的Jons将不受影响。
  7. 复制是快速和简单的,创build一个克隆只是return this 。 由于副本不能改变,假装是自己的副本是安全的。
  8. [编辑,我忘了这一个]。 内部状态可以在对象之间安全共享。 例如,如果你正在实现一个由数组,开始索引和计数支持的列表,那么创build子范围中最昂贵的部分就是复制对象。 但是,如果它是不可变的,那么子范围对象可以引用相同的数组,只有开始索引和计数必须改变,这对构造时间有很大的改变。

总而言之,对于那些没有经历变化的物体来说,它们的目的就是一成不变的。 主要的缺点是需要额外的构造,尽pipe在这里也经常被夸大了(记住,在StringBuilder变得比等效的串联系列更有效率之前,你必须做几个附加的构造)。

如果可变性是对象目标的一部分(谁愿意被薪水永远不会改变的Employee对象build模),那么这将是一个缺点,虽然有时甚至可以是有用的(在许多networking和其他无状态应用程序中,执行读操作的代码与执行更新的代码是分开的,使用不同的对象可能是很自然的 – 我不会让一个对象不可变,然后强制该模式,但是如果我已经有了这个模式,我可能会使我的“读”对象不可变的性能和正确性保证收益)。

写上复制是一个中间立场。 这里“真实”的课程是对“国家”课的引用。 状态类是在复制操作上共享的,但是如果更改状态,则会创build状态类的新副本。 这比C ++更经常用于C ++,这就是为什么它是std:string享有不可变types的一些优点,但不是全部,而且保持可变。

使string不可变有许多优点。 它提供了自动线程安全性,并使string以一种简单,有效的方式performance为一种内在types。 它还允许在运行时提供额外的效率(例如允许有效的string实习来减less资源使用),并具有巨大的安全优势,因为第三方API调用不可能改变string。

为了解决不可变string的一个主要缺点,添加了StringBuilder – 不可变types的运行时构造会导致很大的GC压力,并且本质上很慢。 通过创build一个显式的,可变的类来处理这个问题,这个问题可以在不增加string类的不必要的复杂性的情况下解决。

string并不是真正的不可变的。 他们只是公开的不可改变的。 这意味着你不能从他们的公共界面修改它们。 但在内部实际上是可变的。

如果你不相信我看看使用reflection器的String.Concat定义。 最后一行是…

 int length = str0.Length; string dest = FastAllocateString(length + str1.Length); FillStringChecked(dest, 0, str0); FillStringChecked(dest, length, str1); return dest; 

正如你所看到的FastAllocateString返回一个空的但分配的string,然后它由FillStringChecked

其实FastAllocateString是一个extern方法, FillStringChecked是不安全的,所以它使用指针来复制字节。

也许有更好的例子,但这是我迄今发现的。

stringpipe理是一个昂贵的过程。 保持string不可变允许重复使用string,而不是重新创build。

为什么在C#中stringtypes不可变

string是一个引用types,所以它不会被复制,而是通过引用传递。 将它与通过值传递的C ++ std :: string对象(不可变)进行比较。 这意味着如果你想在一个Hashtable中使用一个string作为关键字,那么在C ++中就可以了,因为C ++会复制string来存储哈希表中的关键字(实际上是std :: hash_map,但是仍然),供以后比较。 所以即使你以后修改了std :: string实例,你也没关系。 但在.Net中,当你在一个Hashtable中使用一个string时,它将存储对该实例的引用。 现在假设string不是不可变的,看看会发生什么:1.有人用键“hello”插入一个值x到一个Hashtable中。 2. Hashtable计算String的哈希值,并将string和值x的引用放入相应的桶中。 3.用户修改String实例为“再见”。 4.现在有人希望散列表中的值为“hello”。 它最终会在正确的桶中查找,但是当比较string时,它会显示“bye”!=“hello”,因此不会返回任何值。 也许有人想要价值“再见”? “再见”可能有不同的哈希值,所以哈希表会在不同的桶中查找。 该桶中没有“再见”键,所以我们的条目仍然没有find。

使string不可变意味着步骤3是不可能的。 如果有人修改string,他将创build一个新的string对象,离开旧的string。 这意味着哈希表中的键仍然是“你好”,因此仍然是正确的。

所以,可能除其他外,不可变string是一种使得通过引用传递的string被用作散列表或类似的字典对象中的键的方式。

你永远不必防守复制不可变的数据。 尽pipe事实上你需要复制它来改变它,但是由于缺乏防御性的复制,通常自由混叠的能力并且不必担心这种混叠的意外后果可以导致更好的性能。

string和其他具体对象通常表示为不可变对象,以提高可读性和运行时效率。 安全性是另一个,一个进程不能改变你的string,并将代码注入到string中

只是为了抛出这个问题,一个经常被遗忘的观点是安全的,如果string是可变的,

 string dir = "C:\SomePlainFolder"; //Kick off another thread GetDirectoryContents(dir); void GetDirectoryContents(string directory) { if(HasAccess(directory) { //Here the other thread changed the string to "C:\AllYourPasswords\" return Contents(directory); } return null; } 

你会发现,如果允许你在string通过后进行变异的话,它会是非常非常糟糕的。

string在.NET中作为引用types传递。

引用types在栈上放置一个指针,指向驻留在托pipe堆上的实际实例。 这与Valuetypes不同,它们将整个实例保存在堆栈中。

当值types作为parameter passing时,运行时会在堆栈上创build值的副本,并将该值传递给方法。 这就是为什么整数必须传递一个'ref'关键字来返回一个更新的值。

当传递引用types时,运行时会在堆栈上创build指针的副本。 该复制的指针仍然指向引用types的原始实例。

stringtypes有一个重载的=运算符,它创build了它自己的副本,而不是指针的副本 – 使其performance得更像一个值types。 但是,如果只复制指针,第二个string操作可能会意外地覆盖另一个类的私有成员的值,从而导致一些相当不愉快的结果。

正如其他职位所提到的,StringBuilder类允许创build没有GC开销的string。

想象一下,你将一个可变string传递给一个函数,但不要指望它被改变。 那么如果函数改变那个string呢? 例如,在C ++中,你可以简单地按值来调用( std::stringstd::string& parameter之间的区别),但是在C#中它是关于引用的,所以如果你在每个函数周围都传递可变string,触发意外的副作用。

这只是其中一个原因。 性能是另一个(例如internedstring)。

在存储类的控制之外,有一个类数据存储数据不能被修改的方法有五种:

  1. 作为值types的基元
  2. 通过持有一个可以自由共享的对类对象的引用,这些类的对象的属性都是不可变的
  3. 通过持有对可变类对象的引用,永远不会暴露任何可能会使任何感兴趣的属性变异的东西
  4. 作为一个结构体,无论是“可变的”还是“不可变的”,它们的所有字段的types都是#1-#4(不是#5)。
  5. 通过持有引用的唯一现存的副本,其对象的属性只能通过该引用进行变异。

因为string的长度是可变的,所以它们不能是值types的基元,也不能将它们的字符数据存储在结构中。 在剩余的select中,唯一不需要将string的字符数据存储在某种不可变对象中的就是#5。 尽pipe围绕选项#5devise框架是可能的,但是这种select将要求任何需要string副本的代码在其控制之外不能被改变时,必须为其自身创build私有副本。 虽然这样做不是不可能的,但是做这些事情所需的额外代码的数量,以及做出所有事情的防御性副本所需的额外运行时间处理的数量远远超过了可能来自string的微小好处, 特别是考虑到有一个可变stringtypes( System.Text.StringBuilder ),它完成了99%的可变string

不可变string还可以防止与并发相关的问题。

想象一下,作为一个使用string的操作系统,其他线程正在修改背后。 你怎么能确认任何东西,而不复制?