为什么.NET String是不可变的?

时间:2010-03-02 17:33:05

标签: c# .net string immutability

众所周知,String是不可改变的。 String不可变的原因是什么,StringBuilder类的引入是可变的?

13 个答案:

答案 0 :(得分:232)

  1. 不可变类型的实例本质上是线程安全的,因为没有线程可以修改它,线程以干扰另一个的方式修改它的风险被删除(引用本身是另一回事)。
  2. 类似地,别名不能产生更改(如果x和y都引用同一个对象,对x的更改需要更改为y)这一事实允许进行大量的编译器优化。
  3. 还可以节省内存优化。实习和雾化是最明显的例子,尽管我们可以做同样原理的其他版本。我曾经通过比较不可变对象和替换重复项的引用来节省大约半GB的内存,这样它们都指向同一个实例(耗费时间,但一分钟的额外启动以节省大量内存是一个在有问题的情况下表现胜利)。使用无法完成的可变对象。
  4. 将不可变类型作为方法传递给参数不会产生任何副作用,除非它是outref(因为它会更改引用,而不是对象)。因此,程序员知道如果string x = "abc"在方法的开头,并且在方法的主体中没有改变,那么在方法结束时x == "abc"
  5. 从概念上讲,语义更像是值类型;特别是平等是基于国家而不是身份。这意味着"abc" == "ab" + "c"。虽然这不需要不变性,但是对这样一个字符串的引用在其整个生命周期中总是等于“abc”(这确实需要不变性)的事实使得使用作为保持与先前值相等的关键是至关重要的,更容易确保正确性(字符串确实常用作键)。
  6. 从概念上讲,变得更有意义。如果我们在圣诞节加一个月,我们没有改变圣诞节,我们在1月下旬创造了一个新的约会。因此Christmas.AddMonths(1)产生一个新的DateTime而不是改变一个可变的return this是有道理的。 (另一个例子,如果我作为一个可变对象改变了我的名字,改变的是我正在使用的名称,“Jon”仍然是不可变的,其他Jons将不会受到影响。
  7. 复制快速而简单,只需{{1}}即可创建克隆。由于副本无论如何都无法改变,假装某些东西是自己的副本是安全的。
  8. [编辑,我忘记了这个]。可以在对象之间安全地共享内部状态。例如,如果您实现的是由数组支持的列表,起始索引和计数,那么创建子范围最昂贵的部分就是复制对象。但是,如果它是不可变的,则子范围对象可以引用相同的数组,只需要更改起始索引和计数,非常对构造时间进行相当大的更改。
  9. 总之,对于没有经历变化的对象,作为其目的的一部分,存在不可变的优点。主要的缺点是需要额外的构造,尽管在这里它经常被夸大(记住,在StringBuilder变得比等效的连接系列更有效之前,你必须做几个附加,以及它们固有的结构)。

    如果可变性是对象目的的一部分(谁想要通过其工资永远不会改变的Employee对象建模),那将是一个缺点,尽管有时甚至它可能是有用的(在许多网站中)和其他无状态应用程序一样,执行读取操作的代码与执行更新的代码是分开的,并且使用不同的对象可能是自然的 - 我不会使对象成为不可变的然后强制该模式,但如果我已经拥有该模式,我可能会使我的“读取“对于性能和正确性保证增益不可变的对象。”

    写时复制是一个中间立场。这里的“真实”类包含对“状态”类的引用。状态类在复制操作上共享,但如果更改状态,则会创建状态类的新副本。这通常与C ++一起使用而不是C#,这就是为什么它的std:string享有不可变类型的一些优点,但不是全部优势,同时保持可变性。

答案 1 :(得分:74)

使字符串不可变具有许多优点。它提供自动线程安全性,并使字符串以简单有效的方式表现为内在类型。它还允许在运行时提高效率(例如允许有效的字符串实习以减少资源使用),并且具有巨大的安全优势,因为第三方API调用不可能更改字符串。

添加StringBuilder是为了解决不可变字符串的一个主要缺点 - 不可变类型的运行时构造会导致很多GC压力并且本质上很慢。通过创建一个显式的可变类来处理这个问题,解决了这个问题,而没有在字符串类中添加不必要的复杂功能。

答案 2 :(得分:20)

字符串不是真正不可变的。它们只是公开不变的。 这意味着您无法从其公共界面修改它们。但在内部实际上是可变的。

如果您不相信我使用reflector查看String.Concat定义。 最后一行是......

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

正如您所看到的,FastAllocateString返回一个空但已分配的字符串,然后由FillStringChecked修改

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

也许有更好的例子,但这是我迄今为止找到的那个。

答案 3 :(得分:13)

字符串管理是一个昂贵的过程。保持字符串不可变允许重复使用重复的字符串,而不是重新创建。

答案 4 :(得分:13)

Why are string types immutable in C#

  

String是一种引用类型,因此它永远不会被复制,而是通过引用传递。   将其与C ++ std :: string进行比较   对象(不是不可变的),哪个   是按值传递的。这意味着如果   你想使用一个字符串作为一个键   Hashtable,你在C ++中很好,因为   C ++将复制字符串以存储   哈希表中的键(实际上是   std :: hash_map,但仍然适用于以后   比较。所以即使你以后   修改std :: string实例,   你没事。但在.Net中,当你使用时   Hashtable中的String,它将存储   对该实例的引用。现在   假设那一串   不是一成不变的,看看是什么   发生:   1.有人用密钥“hello”将值x插入Hashtable。   2. Hashtable计算String的哈希值,并放置一个   引用字符串和值   x在适当的桶中。   3.用户将String实例修改为“bye”。   4.现在有人想要哈希表中与“hello”关联的值。它   最后看着正确的桶,   但在比较它所说的字符串时   “bye”!=“你好”,所以没有价值   回。   也许有人想要“再见”的价值? “再见”可能有所不同   哈希,所以哈希表看起来像   不同的桶。没有“再见”键   那桶,所以我们的进入仍然没有   找到。

     

使字符串不可变意味着   第3步是不可能的。如果有人   修改他正在创建的字符串   新的字符串对象,留下旧的   单独。这意味着关键在于   哈希表仍然是“你好”,因此   仍然是正确的。

     

所以,可能还有其他的事情,   不可变字符串是一种启用方式   通过引用传递的字符串   用作哈希表中的键或   类似的字典对象。

答案 5 :(得分:5)

您永远不必防御性地复制不可变数据。尽管你需要复制它来改变它,但是通常能够自由别名并且不必担心这种混叠的意外后果会因为缺乏防御性复制而导致更好的性能。

答案 6 :(得分:5)

只是为了抛弃它,一个经常被遗忘的视图是安全的,如果字符串是可变的,请记录这个场景:

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;
}

如果你被允许在传递完成后改变字符串,你会看到它是如何非常非常糟糕的。

答案 7 :(得分:4)

字符串在.NET中作为引用类型传递。

引用类型将指针放在堆栈上,驻留在托管堆上的实际实例上。这与Value类型不同,后者将整个实例保存在堆栈中。

当值类型作为参数传递时,运行时会在堆栈上创建值的副本,并将该值传递给方法。这就是为什么必须使用'ref'关键字传递整数才能返回更新的值。

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

字符串类型有一个重载的=运算符,它创建自身的副本,而不是指针的副本 - 使其行为更像值类型。但是,如果只复制了指针,第二个字符串操作可能会意外地覆盖另一个类的私有成员的值,从而导致一些非常讨厌的结果。

正如其他帖子所提到的,StringBuilder类允许创建没有GC开销的字符串。

答案 8 :(得分:3)

字符串和其他具体对象通常表示为不可变对象,以提高可读性和运行时效率。安全性是另一个,进程无法更改您的字符串并将代码注入字符串

答案 9 :(得分:3)

想象一下,你将一个可变字符串传递给一个函数,但不要指望它被改变。那么如果函数改变了那个字符串呢?例如,在C ++中,您可以简单地执行按值调用(std::stringstd::string&参数之间的差异),但在C#中它是关于引用的所有内容,因此如果您在每个函数周围传递可变字符串都可能会更改它会引发意想不到的副作用。

这只是众多原因之一。性能是另一个(例如,实习字符串)。

答案 10 :(得分:2)

类数据存储数据的五种常用方法是无法在存储类控件之外修改数据:

  1. 作为值类型原语
  2. 通过对类对象保持可自由共享的引用,其对象的属性都是不可变的
  3. 通过保持对可变类对象的引用,该对象永远不会暴露于可能会改变任何感兴趣属性的任何内容
  4. 作为结构,无论是“可变的”还是“不可变的”,其所有字段都是#1-#4类型(不是#5)。
  5. 通过保存对象的引用的唯一现存副本,该对象的属性只能通过该引用进行变异。

因为字符串具有可变长度,所以它们不能是值类型原语,也不能将它们的字符数据存储在结构中。在剩下的选择中,唯一不需要将字符串的字符数据存储在某种不可变对象中的将是#5。虽然可以围绕选项#5设计一个框架,但是这个选择要求任何想要在其控制之外无法更改的字符串副本的代码必须为自己制作一个私有副本。虽然几乎不可能做到这一点,但需要额外代码的数量,以及制作所有内容的防御性副本所需的额外运行时处理量,远远超过了{{1鉴于有一个可变的字符串类型(string)可以完成99%可变的System.Text.StringBuilder

是可变的,尤其是

答案 11 :(得分:0)

不可变字符串也可以防止与并发相关的问题。

答案 12 :(得分:0)

想象一下,使用一个其他线程的字符串操作系统 背后修改。如果没有,你怎么能验证 制作副本?